mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-31 21:12:58 +03:00
Compare commits
49 Commits
fix/killsw
...
feature/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e99d1d5d | ||
|
|
661fca64dd | ||
|
|
0f6847219b | ||
|
|
5d16645b84 | ||
|
|
203a092dc9 | ||
|
|
d8b8590bc4 | ||
|
|
9b8bfaa6f8 | ||
|
|
890103a16a | ||
|
|
56ab82f87f | ||
|
|
3984acbb44 | ||
|
|
cc404378f9 | ||
|
|
be4df3594b | ||
|
|
5c8c143833 | ||
|
|
24f545d001 | ||
|
|
2b0d86c626 | ||
|
|
fe773a108e | ||
|
|
a0997156f6 | ||
|
|
9a6e975622 | ||
|
|
78f09634a4 | ||
|
|
be692001b0 | ||
|
|
850b8ea03b | ||
|
|
c645d07a87 | ||
|
|
7f8786720e | ||
|
|
25c70b5bf6 | ||
|
|
7bf16f075c | ||
|
|
6518d4866e | ||
|
|
4c2010244b | ||
|
|
e946ee2430 | ||
|
|
5fab8363e7 | ||
|
|
35c2e1564b | ||
|
|
ca5bca085b | ||
|
|
33b2a3d2fc | ||
|
|
917f5858a8 | ||
|
|
e98c11079a | ||
|
|
84d908d4d8 | ||
|
|
412e69af9b | ||
|
|
4492b0af7e | ||
|
|
806b1d75af | ||
|
|
9740e7557a | ||
|
|
5ab82e2196 | ||
|
|
ae44de7101 | ||
|
|
2be6079e21 | ||
|
|
2a653f8876 | ||
|
|
41ab51a5ef | ||
|
|
300558c33c | ||
|
|
774deb87f5 | ||
|
|
bae7fbd222 | ||
|
|
97e4b95673 | ||
|
|
2ae97c5cda |
14
.github/workflows/deploy.yml
vendored
14
.github/workflows/deploy.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
run: pip install "conan==2.28.0"
|
||||
|
||||
- name: 'Build dependencies'
|
||||
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
|
||||
run: cmake -S . -B build -DPREBUILTS_ONLY=1
|
||||
|
||||
- name: 'Authorize in remote'
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -365,7 +365,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'ios'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -506,7 +506,7 @@ jobs:
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -739,7 +739,7 @@ jobs:
|
||||
ANDROID_PLATFORM: android-28
|
||||
NDK_VERSION: 27.0.11718014
|
||||
QT_VERSION: 6.10.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools qthttpserver qtwebsockets'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,7 +10,9 @@ deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
.cursorignore
|
||||
.cursor/
|
||||
.venv/
|
||||
|
||||
# Qt-es
|
||||
/.qmake.cache
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.2)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.3)
|
||||
|
||||
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 2123)
|
||||
set(APP_ANDROID_VERSION_CODE 2126)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -14,6 +14,10 @@ set(PACKAGES
|
||||
Core5Compat Concurrent LinguistTools
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND PACKAGES HttpServer)
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMAND git rev-parse --short HEAD
|
||||
@@ -47,6 +51,10 @@ set(LIBS ${LIBS}
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND LIBS Qt6::HttpServer)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
endif()
|
||||
|
||||
@@ -119,7 +119,13 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
|
||||
win->show();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:angle="135"
|
||||
android:startColor="#2A2A2E"
|
||||
android:centerColor="#17171A"
|
||||
android:endColor="#0E0E11" />
|
||||
</shape>
|
||||
7
client/android/res/drawable/ic_launcher_monochrome.xml
Normal file
7
client/android/res/drawable/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/ic_amnezia_round"
|
||||
android:insetLeft="19.5%"
|
||||
android:insetTop="19.5%"
|
||||
android:insetRight="19.5%"
|
||||
android:insetBottom="19.5%" />
|
||||
6
client/android/res/mipmap-anydpi-v26/icon.xml
Normal file
6
client/android/res/mipmap-anydpi-v26/icon.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
6
client/android/res/mipmap-anydpi-v26/icon_round.xml
Normal file
6
client/android/res/mipmap-anydpi-v26/icon_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
BIN
client/android/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
client/android/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
client/android/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
client/android/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
client/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
BIN
client/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
16
client/android/res/values-v31/styles.xml
Normal file
16
client/android/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="NoActionBar">
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
<item name="android:enforceStatusBarContrast">false</item>
|
||||
|
||||
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
client/android/res/values/colors.xml
Normal file
4
client/android/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0E0E11</color>
|
||||
</resources>
|
||||
@@ -152,5 +152,5 @@ message(${QtCore_location})
|
||||
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
|
||||
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
|
||||
)
|
||||
|
||||
@@ -201,6 +201,11 @@ file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/configurat
|
||||
file(GLOB_RECURSE CORE_MODELS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.h)
|
||||
file(GLOB_RECURSE CORE_MODELS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/models/*.cpp)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
file(GLOB LOCAL_PROXY_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.h)
|
||||
file(GLOB LOCAL_PROXY_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/core/local-proxy/*.cpp)
|
||||
endif()
|
||||
|
||||
file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
|
||||
${CLIENT_ROOT_DIR}/ui/models/*.h
|
||||
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
|
||||
@@ -246,6 +251,11 @@ set(SOURCES ${SOURCES}
|
||||
${UI_CONTROLLERS_CPP}
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT IOS)
|
||||
list(APPEND HEADERS ${LOCAL_PROXY_H})
|
||||
list(APPEND SOURCES ${LOCAL_PROXY_CPP})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/protocols/ikev2VpnProtocolWindows.h
|
||||
|
||||
@@ -244,11 +244,7 @@ ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials
|
||||
<< "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");
|
||||
}
|
||||
|
||||
const QString flowValue = srv.flow;
|
||||
QString realityPublicKey;
|
||||
QString realityShortId;
|
||||
if (srv.security == QLatin1String("reality")) {
|
||||
@@ -563,9 +559,12 @@ QJsonObject XrayConfigurator::buildStreamSettings(const XrayServerConfig &srv, c
|
||||
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();
|
||||
const int fromV = pad.bytesMin.isEmpty() ? 1 : pad.bytesMin.toInt();
|
||||
int toV = pad.bytesMax.isEmpty() ? 256 : pad.bytesMax.toInt();
|
||||
if (toV < fromV)
|
||||
toV = fromV;
|
||||
br[QStringLiteral("from")] = fromV;
|
||||
br[QStringLiteral("to")] = toV;
|
||||
xo[QStringLiteral("xPaddingBytes")] = br;
|
||||
}
|
||||
xo[QStringLiteral("xPaddingKey")] = pad.key.isEmpty() ? QStringLiteral("x_padding") : pad.key;
|
||||
|
||||
@@ -106,7 +106,8 @@ ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) c
|
||||
return ErrorCode::AmneziaServiceNotRunning;
|
||||
}
|
||||
|
||||
if (serverConfigUtils::isLegacyApiSubscription(m_serversRepository->serverKind(serverId))) {
|
||||
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
|
||||
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
|
||||
return ErrorCode::LegacyApiV1NotSupportedError;
|
||||
}
|
||||
|
||||
@@ -117,6 +118,9 @@ ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) c
|
||||
}
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
if (serverConfigUtils::isApiV2Subscription(kind)) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
return ErrorCode::NoInstalledContainersError;
|
||||
}
|
||||
|
||||
@@ -214,6 +218,13 @@ ErrorCode ConnectionController::openConnection(const QString &serverId)
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit localProxyStoppedBecauseVpnTurnedOn(tr("Local proxy stopped because VPN was turned on"));
|
||||
}
|
||||
#endif
|
||||
|
||||
emit openConnectionRequested(serverId, container, vpnConfiguration);
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ signals:
|
||||
void closeConnectionRequested();
|
||||
void setConnectionStateRequested(Vpn::ConnectionState state);
|
||||
void killSwitchModeChangedRequested(bool enabled);
|
||||
void localProxyStoppedBecauseVpnTurnedOn(const QString &message);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
void restoreConnectionRequested();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "coreController.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QDebug>
|
||||
#include <QTranslator>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
@@ -35,6 +38,10 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
|
||||
initAppleController();
|
||||
initLogging();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
initLocalProxy();
|
||||
#endif
|
||||
|
||||
m_translator = new QTranslator(this);
|
||||
if (m_appSettingsRepository) {
|
||||
updateTranslator(m_appSettingsRepository->getAppLanguage());
|
||||
@@ -48,6 +55,69 @@ void CoreController::setQmlContextProperty(const QString &name, QObject *value)
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void CoreController::initLocalProxy()
|
||||
{
|
||||
constexpr quint16 kLocalProxyApiPort = 49490;
|
||||
|
||||
m_proxyServer.reset(new ProxyServer(m_serversRepository, m_appSettingsRepository, this));
|
||||
|
||||
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
auto syncLocalProxy = [this]() {
|
||||
if (!m_proxyServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool httpEnabled = m_appSettingsRepository->isLocalProxyHttpEnabled();
|
||||
|
||||
if (!httpEnabled) {
|
||||
qInfo() << "Local proxy: HTTP API disabled";
|
||||
m_proxyServer->stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_proxyServer->start(kLocalProxyApiPort)) {
|
||||
qWarning() << "Local proxy: failed to start on port" << kLocalProxyApiPort;
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit m_appSettingsRepository->localProxyStartFailed(tr("Local proxy failed to start. Check if the port is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_proxyServer->syncSettings()) {
|
||||
qWarning() << "Local proxy: failed to start proxy core (Xray)";
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
emit m_appSettingsRepository->localProxyStartFailed(tr("Couldn’t start the proxy due to an internal error. Try restarting the app."));
|
||||
return;
|
||||
}
|
||||
|
||||
qInfo() << "Local proxy: running on 127.0.0.1:" << kLocalProxyApiPort;
|
||||
};
|
||||
|
||||
syncLocalProxy();
|
||||
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this, syncLocalProxy);
|
||||
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, [this](const QString &serverId) {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->isLocalProxyHttpEnabled()
|
||||
&& m_appSettingsRepository->localProxyOwnerId() == serverId) {
|
||||
m_appSettingsRepository->bumpLocalProxyRestartToken();
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this, [this](const QString &serverId, int) {
|
||||
if (m_appSettingsRepository && m_appSettingsRepository->localProxyOwnerId() == serverId) {
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
void CoreController::initModels()
|
||||
{
|
||||
m_containersModel = new ContainersModel(this);
|
||||
@@ -197,6 +267,11 @@ void CoreController::initControllers()
|
||||
m_pageController = new PageController(m_serversController, m_settingsController, this);
|
||||
setQmlContextProperty("PageController", m_pageController);
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
connect(m_connectionController, &ConnectionController::localProxyStoppedBecauseVpnTurnedOn, m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
#endif
|
||||
|
||||
m_serversUiController = new ServersUiController(m_serversController, m_settingsController, m_serversModel, m_containersModel, m_defaultServerContainersModel, this);
|
||||
setQmlContextProperty("ServersUiController", m_serversUiController);
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "core/local-proxy/proxyserver.h"
|
||||
#include "ui/utils/notificationHandler.h"
|
||||
#endif
|
||||
|
||||
@@ -142,6 +143,9 @@ private:
|
||||
void initAppleController();
|
||||
void initLogging();
|
||||
void initSignalHandlers();
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void initLocalProxy();
|
||||
#endif
|
||||
void setQmlContextProperty(const QString &name, QObject *value);
|
||||
|
||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||
@@ -227,6 +231,10 @@ private:
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
QScopedPointer<ProxyServer> m_proxyServer;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // CORECONTROLLER_H
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/utils/errorCodes.h"
|
||||
@@ -144,7 +145,9 @@ void CoreSignalHandlers::initExportControllerHandler()
|
||||
});
|
||||
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
|
||||
[this](const QString &serverId, int row, DockerContainer container) {
|
||||
m_coreController->m_usersController->revokeClient(serverId, row, container);
|
||||
QtConcurrent::run([this, serverId, row, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, row, container);
|
||||
});
|
||||
});
|
||||
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
|
||||
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
|
||||
@@ -202,13 +205,15 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
|
||||
{
|
||||
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
|
||||
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
QtConcurrent::run([this, serverId, containerConfig, container]() {
|
||||
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
|
||||
});
|
||||
});
|
||||
|
||||
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
|
||||
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
|
||||
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
|
||||
});
|
||||
}, Qt::DirectConnection);
|
||||
|
||||
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
|
||||
&InstallController::clearCachedProfile);
|
||||
@@ -285,6 +290,8 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
|
||||
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
|
||||
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
|
||||
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
|
||||
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
|
||||
m_coreController->m_exportController, &ExportController::revokeFinished);
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initSitesModelUpdateHandler()
|
||||
|
||||
@@ -48,6 +48,7 @@ signals:
|
||||
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
|
||||
void updateClientsRequested(const QString &serverId, DockerContainer container);
|
||||
void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
|
||||
void revokeFinished(ErrorCode errorCode);
|
||||
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -234,7 +234,9 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
clearCachedProfile(serverId, container);
|
||||
if (reinstallRequired) {
|
||||
clearCachedProfile(serverId, container);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
}
|
||||
@@ -836,8 +838,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
|
||||
|
||||
if (container == DockerContainer::Awg2) {
|
||||
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = regex.match(stdOut);
|
||||
QRegularExpression kernelVersionRegex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||
QRegularExpressionMatch match = kernelVersionRegex.match(stdOut);
|
||||
if (match.hasMatch()) {
|
||||
int majorVersion = match.captured(1).toInt();
|
||||
int minorVersion = match.captured(2).toInt();
|
||||
@@ -850,8 +852,19 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
|
||||
if (stdOut.contains("lock"))
|
||||
return ErrorCode::ServerPacketManagerError;
|
||||
if (stdOut.contains("command not found"))
|
||||
if (stdOut.contains("Container runtime is not supported"))
|
||||
return ErrorCode::ServerContainerRuntimeNotSupported;
|
||||
|
||||
QRegularExpression notFoundRegex(
|
||||
R"(^.*(?:sudo:|docker:).*not found.*$)",
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
if (notFoundRegex.match(stdOut).hasMatch()) {
|
||||
return ErrorCode::ServerDockerFailedError;
|
||||
}
|
||||
|
||||
if (stdOut.contains("Container runtime service not running"))
|
||||
return ErrorCode::ContainerRuntimeServiceNotRunning;
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -888,7 +901,7 @@ ErrorCode InstallController::isUserInSudo(const ServerCredentials &credentials,
|
||||
return ErrorCode::ServerUserNotInSudo;
|
||||
if (stdOut.contains("can't cd to") || stdOut.contains("Permission denied") || stdOut.contains("No such file or directory"))
|
||||
return ErrorCode::ServerUserDirectoryNotAccessible;
|
||||
if (stdOut.contains("sudoers") || stdOut.contains("is not allowed to run sudo on"))
|
||||
if (stdOut.contains(QRegularExpression(R"(\bsudoers\b)")) || stdOut.contains("is not allowed to") || stdOut.contains("can't do that"))
|
||||
return ErrorCode::ServerUserNotAllowedInSudoers;
|
||||
if (stdOut.contains("password is required") || stdOut.contains("authentication is required"))
|
||||
return ErrorCode::ServerUserPasswordRequired;
|
||||
|
||||
@@ -698,7 +698,7 @@ ErrorCode UsersController::revokeXray(const int row,
|
||||
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
error = sshSession->runScript(
|
||||
credentials,
|
||||
credentials,
|
||||
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
|
||||
);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -758,14 +758,17 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
|
||||
ContainerConfig containerCfg = adminConfig->containerConfig(container);
|
||||
QString containerClientId = containerCfg.protocolConfig.clientId();
|
||||
|
||||
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
|
||||
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
|
||||
if (isAdminMatch) {
|
||||
emit adminConfigRevoked(serverId, container);
|
||||
}
|
||||
|
||||
emit clientRevoked(index);
|
||||
emit clientsUpdated(m_clientsTable);
|
||||
}
|
||||
|
||||
emit clientsUpdated(m_clientsTable);
|
||||
emit revokeFinished(errorCode);
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ signals:
|
||||
void clientAdded(const QJsonObject &client);
|
||||
void clientRenamed(int row, const QString &newName);
|
||||
void clientRevoked(int row);
|
||||
void revokeFinished(ErrorCode errorCode);
|
||||
void adminConfigRevoked(const QString &serverId, DockerContainer container);
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -8,10 +8,19 @@
|
||||
#include "version.h"
|
||||
#include "ui/utils/qAutoStart.h"
|
||||
#include "logger.h"
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "core/local-proxy/portavailabilityhelper.h"
|
||||
#endif
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kDefaultProxyPort = 10808;
|
||||
constexpr int kLocalProxyPortMin = 1024;
|
||||
constexpr int kLocalProxyPortMax = 65535;
|
||||
}
|
||||
|
||||
QString getPlatformName()
|
||||
{
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
@@ -38,6 +47,11 @@ SettingsController::SettingsController(SecureServersRepository* serversRepositor
|
||||
{
|
||||
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
|
||||
m_isDevModeEnabled = m_appSettingsRepository->isDevGatewayEnv();
|
||||
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxySettingsChanged, this,
|
||||
&SettingsController::localProxySettingsUpdated);
|
||||
connect(m_appSettingsRepository, &SecureAppSettingsRepository::localProxyStartFailed, this,
|
||||
&SettingsController::localProxyStartFailed);
|
||||
}
|
||||
|
||||
void SettingsController::toggleAmneziaDns(bool enable)
|
||||
@@ -366,3 +380,114 @@ QString SettingsController::nextAvailableServerName() const
|
||||
return m_serversRepository->nextAvailableServerName();
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxySupported() const
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyHttpEnabled() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyHttpEnabled();
|
||||
}
|
||||
|
||||
int SettingsController::localProxyPort() const
|
||||
{
|
||||
return static_cast<int>(m_appSettingsRepository->localProxyPort());
|
||||
}
|
||||
|
||||
QString SettingsController::localProxyOwnerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
bool SettingsController::setLocalProxyPort(int port)
|
||||
{
|
||||
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() == static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsController::isLocalProxyPortBusy(int port) const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
return !PortAvailabilityHelper::isPortAvailable(port);
|
||||
#else
|
||||
Q_UNUSED(port);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int SettingsController::findFirstAvailableLocalProxyPort(int startPort) const
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
const auto port = PortAvailabilityHelper::findFirstAvailablePort(startPort, kLocalProxyPortMax);
|
||||
return port ? *port : -1;
|
||||
#else
|
||||
Q_UNUSED(startPort);
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SettingsController::enableLocalProxy(const QString &ownerId, int port)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
Q_UNUSED(ownerId);
|
||||
Q_UNUSED(port);
|
||||
return false;
|
||||
#else
|
||||
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyHttpEnabled() && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int selectedPort = port;
|
||||
|
||||
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
if (isUserDefinedPort) {
|
||||
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
return false;
|
||||
}
|
||||
} else if (selectedPort != kDefaultProxyPort && !PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(selectedPort)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(selectedPort));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(isUserDefinedPort);
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(true);
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SettingsController::disableLocalProxy()
|
||||
{
|
||||
if (m_appSettingsRepository->isLocalProxyHttpEnabled()) {
|
||||
m_appSettingsRepository->setLocalProxyHttpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ public:
|
||||
QLocale getAppLanguage() const;
|
||||
void setAppLanguage(const QLocale &locale);
|
||||
|
||||
// Local proxy
|
||||
bool isLocalProxySupported() const;
|
||||
bool isLocalProxyHttpEnabled() const;
|
||||
int localProxyPort() const;
|
||||
QString localProxyOwnerId() const;
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
bool setLocalProxyPort(int port);
|
||||
bool isLocalProxyPortBusy(int port) const;
|
||||
int findFirstAvailableLocalProxyPort(int startPort) const;
|
||||
bool enableLocalProxy(const QString &ownerId, int port);
|
||||
void disableLocalProxy();
|
||||
|
||||
signals:
|
||||
void siteSplitTunnelingRouteModeChanged(RouteMode mode);
|
||||
void siteSplitTunnelingToggled(bool enabled);
|
||||
@@ -96,6 +108,9 @@ signals:
|
||||
void appSplitTunnelingToggled(bool enabled);
|
||||
void appSplitTunnelingClearAppsList();
|
||||
|
||||
void localProxySettingsUpdated();
|
||||
void localProxyStartFailed(const QString &message);
|
||||
|
||||
private:
|
||||
QString getPlatform() const;
|
||||
|
||||
|
||||
444
client/core/local-proxy/configmanager.cpp
Normal file
444
client/core/local-proxy/configmanager.cpp
Normal file
@@ -0,0 +1,444 @@
|
||||
#include "configmanager.h"
|
||||
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "portavailabilityhelper.h"
|
||||
#include "proxylogger.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QSaveFile>
|
||||
#include <QSysInfo>
|
||||
#include <QStandardPaths>
|
||||
#include <QUuid>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
ConfigManager::ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository)
|
||||
: m_serversRepository(serversRepository), m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("ConfigManager initialized (repository-backed)");
|
||||
}
|
||||
|
||||
namespace {
|
||||
namespace gateway_key {
|
||||
constexpr char vless[] = "vless";
|
||||
} // namespace gateway_key
|
||||
|
||||
constexpr quint16 kDefaultProxyPort = 10808;
|
||||
constexpr int kProxyPortMin = 1024;
|
||||
constexpr int kProxyPortMax = 65535;
|
||||
|
||||
int resolveProxyPort(SecureAppSettingsRepository *appSettings)
|
||||
{
|
||||
if (!appSettings) {
|
||||
return kDefaultProxyPort;
|
||||
}
|
||||
|
||||
const quint16 port = appSettings->localProxyPort();
|
||||
if (port < kProxyPortMin || port > kProxyPortMax) {
|
||||
return kDefaultProxyPort;
|
||||
}
|
||||
|
||||
return static_cast<int>(port);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ConfigManager::applyProxyPortToConfig(QJsonObject &config, int port) const
|
||||
{
|
||||
if (!config.contains("inbounds") || !config.value("inbounds").isArray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = config.value("inbounds").toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
firstInbound.insert("port", port);
|
||||
inbounds[0] = firstInbound;
|
||||
config.insert("inbounds", inbounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ConfigManager::serializeConfig(const QJsonObject &config) const
|
||||
{
|
||||
return QString::fromUtf8(QJsonDocument(config).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfig(QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_serversRepository || !m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("Local proxy repositories are not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
const QString message = QStringLiteral("Local proxy owner server id is not configured");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
const QString message = QStringLiteral("Server %1 lacks Xray last_config payload")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigData data;
|
||||
data.ownerId = ownerId;
|
||||
data.serverName = ownerServer->value(configKey::name).toString();
|
||||
data.parsedConfig = doc.object();
|
||||
const int proxyPort = resolveProxyPort(m_appSettingsRepository);
|
||||
if (applyProxyPortToConfig(data.parsedConfig, proxyPort)) {
|
||||
data.serializedConfig = serializeConfig(data.parsedConfig);
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
|
||||
data.serializedConfig = *serializedConfig;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::optional<ConfigManager::ConfigData> ConfigManager::buildConfigWithFetch(QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_serversRepository || !m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("Local proxy repositories are not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
const QString message = QStringLiteral("Local proxy owner server id is not configured");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
const QString message = QStringLiteral("Owner server with id %1 not found").arg(ownerId);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
const QString message = QStringLiteral("Server %1 is not premium, local proxy is unavailable")
|
||||
.arg(ownerServer->value(configKey::name).toString());
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = extractSerializedXrayConfig(*ownerServer);
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
auto fetchedConfig = fetchSerializedXrayConfigFromGateway(*ownerServer, errorDescription);
|
||||
if (!fetchedConfig || fetchedConfig->isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
serializedConfig = fetchedConfig;
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse Xray config JSON: %1").arg(parseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigData data;
|
||||
data.ownerId = ownerId;
|
||||
data.serverName = ownerServer->value(configKey::name).toString();
|
||||
data.parsedConfig = doc.object();
|
||||
|
||||
int selectedPort = resolveProxyPort(m_appSettingsRepository);
|
||||
const bool isUserDefinedPort = m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
|
||||
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
|
||||
const bool canAutoSelect = !isUserDefinedPort && selectedPort == kDefaultProxyPort;
|
||||
if (canAutoSelect) {
|
||||
const auto freePort = PortAvailabilityHelper::findFirstAvailablePort(kDefaultProxyPort + 1, kProxyPortMax);
|
||||
if (!freePort) {
|
||||
errorDescription = QStringLiteral("No available local proxy port in range %1-%2")
|
||||
.arg(kDefaultProxyPort + 1)
|
||||
.arg(kProxyPortMax);
|
||||
ProxyLogger::getInstance().error(errorDescription);
|
||||
return std::nullopt;
|
||||
}
|
||||
selectedPort = *freePort;
|
||||
} else {
|
||||
errorDescription = QStringLiteral("Local proxy port %1 is already in use")
|
||||
.arg(selectedPort);
|
||||
ProxyLogger::getInstance().error(errorDescription);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (applyProxyPortToConfig(data.parsedConfig, selectedPort)) {
|
||||
data.serializedConfig = serializeConfig(data.parsedConfig);
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
|
||||
data.serializedConfig = *serializedConfig;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool ConfigManager::writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
configPath.clear();
|
||||
|
||||
const QString directory = tempDirectory();
|
||||
if (!QDir().mkpath(directory)) {
|
||||
const QString message = QStringLiteral("Failed to create temp config directory: %1").arg(directory);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString path = tempConfigPath();
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
const QString message = QStringLiteral("Failed to open temp config file %1: %2").arg(path, file.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.write(serializedConfig.toUtf8()) == -1) {
|
||||
const QString message = QStringLiteral("Failed to write temp config file %1: %2").arg(path, file.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.commit()) {
|
||||
const QString message = QStringLiteral("Failed to commit temp config file %1").arg(path);
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info(QStringLiteral("Xray config saved to %1").arg(path));
|
||||
configPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::removeTempConfig() const
|
||||
{
|
||||
const QString path = tempConfigPath();
|
||||
QFile file(path);
|
||||
if (!file.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.remove()) {
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to remove temp config file %1: %2").arg(path, file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().debug(QStringLiteral("Removed temp config file %1").arg(path));
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ConfigManager::tempConfigPath() const
|
||||
{
|
||||
return QDir(tempDirectory()).filePath(QStringLiteral("xray_active.json"));
|
||||
}
|
||||
|
||||
std::optional<QString> ConfigManager::extractSerializedXrayConfig(const QJsonObject &server) const
|
||||
{
|
||||
const QJsonArray containers = server.value(configKey::containers).toArray();
|
||||
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
|
||||
const QString protoKey = QString(configKey::xray);
|
||||
|
||||
for (const QJsonValue &value : containers) {
|
||||
const QJsonObject container = value.toObject();
|
||||
if (container.value(configKey::container).toString() != targetContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject proto = container.value(protoKey).toObject();
|
||||
const QString serialized = proto.value(configKey::lastConfig).toString();
|
||||
if (!serialized.isEmpty()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<QString> ConfigManager::fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const
|
||||
{
|
||||
errorDescription.clear();
|
||||
|
||||
if (!m_appSettingsRepository) {
|
||||
const QString message = QStringLiteral("App settings repository is not available");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject apiConfig = server.value(apiDefs::key::apiConfig).toObject();
|
||||
if (apiConfig.isEmpty()) {
|
||||
const QString message = QStringLiteral("Server API config is missing");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QString userCountryCode = apiConfig.value(apiDefs::key::userCountryCode).toString();
|
||||
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
|
||||
if (userCountryCode.isEmpty() || serviceType.isEmpty()) {
|
||||
const QString message = QStringLiteral("Server API config lacks service identifiers");
|
||||
ProxyLogger::getInstance().warning(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject apiPayload;
|
||||
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
|
||||
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
|
||||
|
||||
const QString appLanguage = m_appSettingsRepository->getAppLanguage().name().split("_").first();
|
||||
if (!appLanguage.isEmpty()) {
|
||||
apiPayload[apiDefs::key::appLanguage] = appLanguage;
|
||||
}
|
||||
|
||||
apiPayload[apiDefs::key::uuid] = m_appSettingsRepository->getInstallationUuid(true);
|
||||
apiPayload[apiDefs::key::userCountryCode] = userCountryCode;
|
||||
apiPayload[apiDefs::key::serviceType] = serviceType;
|
||||
apiPayload[apiDefs::key::serviceProtocol] = QString(gateway_key::vless);
|
||||
apiPayload[apiDefs::key::publicKey] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
const QJsonObject authData = server.value(apiDefs::key::authData).toObject();
|
||||
if (!authData.isEmpty()) {
|
||||
apiPayload[apiDefs::key::authData] = authData;
|
||||
}
|
||||
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs, m_appSettingsRepository->isStrictKillSwitchEnabled());
|
||||
|
||||
QByteArray responseBody;
|
||||
const amnezia::ErrorCode errorCode = gatewayController.post(QString("%1v1/config"), apiPayload, responseBody);
|
||||
if (errorCode != amnezia::ErrorCode::NoError) {
|
||||
const QString message = QStringLiteral("Gateway request failed with error code %1").arg(static_cast<int>(errorCode));
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError responseError;
|
||||
const QJsonDocument responseDoc = QJsonDocument::fromJson(responseBody, &responseError);
|
||||
if (responseError.error != QJsonParseError::NoError || !responseDoc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse gateway response: %1").arg(responseError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QString data = responseDoc.object().value(configKey::config).toString();
|
||||
if (data.isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway response lacks config payload");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
data.replace("vpn://", "");
|
||||
QByteArray decoded = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
if (decoded.isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway config payload is empty");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QByteArray uncompressed = qUncompress(decoded);
|
||||
if (!uncompressed.isEmpty()) {
|
||||
decoded = uncompressed;
|
||||
}
|
||||
|
||||
QJsonParseError configError;
|
||||
const QJsonDocument configDoc = QJsonDocument::fromJson(decoded, &configError);
|
||||
if (configError.error != QJsonParseError::NoError || !configDoc.isObject()) {
|
||||
const QString message = QStringLiteral("Failed to parse gateway config JSON: %1").arg(configError.errorString());
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = extractSerializedXrayConfig(configDoc.object());
|
||||
if (!serializedConfig || serializedConfig->isEmpty()) {
|
||||
const QString message = QStringLiteral("Gateway response lacks Xray last_config payload");
|
||||
ProxyLogger::getInstance().error(message);
|
||||
errorDescription = message;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Fetched Xray config from gateway");
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
QString ConfigManager::tempDirectory() const
|
||||
{
|
||||
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
if (baseDir.isEmpty()) {
|
||||
return QDir::temp().filePath(QStringLiteral("amnezia_local_proxy"));
|
||||
}
|
||||
return QDir(baseDir).filePath(QStringLiteral("local_proxy"));
|
||||
}
|
||||
37
client/core/local-proxy/configmanager.h
Normal file
37
client/core/local-proxy/configmanager.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
struct ConfigData {
|
||||
QString ownerId;
|
||||
QString serverName;
|
||||
QString serializedConfig;
|
||||
QJsonObject parsedConfig;
|
||||
};
|
||||
|
||||
ConfigManager(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository);
|
||||
|
||||
std::optional<ConfigData> buildConfig(QString &errorDescription) const;
|
||||
std::optional<ConfigData> buildConfigWithFetch(QString &errorDescription) const;
|
||||
bool writeTempConfig(const QString &serializedConfig, QString &configPath, QString &errorDescription) const;
|
||||
bool removeTempConfig() const;
|
||||
QString tempConfigPath() const;
|
||||
|
||||
private:
|
||||
std::optional<QString> extractSerializedXrayConfig(const QJsonObject &server) const;
|
||||
std::optional<QString> fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const;
|
||||
QString tempDirectory() const;
|
||||
bool applyProxyPortToConfig(QJsonObject &config, int port) const;
|
||||
QString serializeConfig(const QJsonObject &config) const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
};
|
||||
190
client/core/local-proxy/httpapi.cpp
Normal file
190
client/core/local-proxy/httpapi.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "httpapi.h"
|
||||
#include "proxylogger.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QHostAddress>
|
||||
#include <optional>
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> extractInboundPort(const QJsonObject &config)
|
||||
{
|
||||
if (!config.contains("inbounds") || !config["inbounds"].isArray()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonArray inbounds = config["inbounds"].toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
if (!firstInbound.contains("port")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return firstInbound.value("port").toInt();
|
||||
}
|
||||
|
||||
QJsonValue proxyPortValue(const std::optional<int> &port)
|
||||
{
|
||||
if (port.has_value()) {
|
||||
return QJsonValue(*port);
|
||||
}
|
||||
return QJsonValue::Null;
|
||||
}
|
||||
|
||||
QHttpServerResponse makeServiceUnavailablePingResponse()
|
||||
{
|
||||
QJsonObject payload{
|
||||
{"status", "error"},
|
||||
{"proxyPort", QJsonValue::Null}
|
||||
};
|
||||
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
|
||||
}
|
||||
|
||||
QHttpServerResponse makeServiceUnavailableStatusResponse()
|
||||
{
|
||||
QJsonObject payload{
|
||||
{"status", "error"}
|
||||
};
|
||||
return QHttpServerResponse(payload, QHttpServerResponse::StatusCode::ServiceUnavailable);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HttpApi::HttpApi(QWeakPointer<IProxyService> service, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_tcpServer(new QTcpServer(this))
|
||||
, m_service(service)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("HttpApi initialized");
|
||||
}
|
||||
|
||||
HttpApi::~HttpApi()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool HttpApi::start(quint16 port)
|
||||
{
|
||||
ProxyLogger::getInstance().info(QString("Starting HTTP API server on port %1").arg(port));
|
||||
|
||||
if (!m_tcpServer->listen(QHostAddress::LocalHost, port)) {
|
||||
ProxyLogger::getInstance().error(QString("Failed to start HTTP API server on port %1").arg(port));
|
||||
return false;
|
||||
}
|
||||
|
||||
setupRoutes();
|
||||
m_server.bind(m_tcpServer.data());
|
||||
|
||||
ProxyLogger::getInstance().info(QString("HTTP API server is running on localhost:%1").arg(m_tcpServer->serverPort()));
|
||||
ProxyLogger::getInstance().debug("Available endpoints:\n"
|
||||
" POST /api/v1/up\n"
|
||||
" POST /api/v1/down\n"
|
||||
" GET /api/v1/ping");
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpApi::stop()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Stopping HTTP API server");
|
||||
if (m_tcpServer) {
|
||||
m_tcpServer->close();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpApi::setupRoutes()
|
||||
{
|
||||
ProxyLogger::getInstance().debug("Setting up HTTP API routes");
|
||||
|
||||
m_server.route("/api/v1/up", QHttpServerRequest::Method::Post,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling POST /api/v1/up request");
|
||||
return handlePostUp();
|
||||
});
|
||||
|
||||
m_server.route("/api/v1/down", QHttpServerRequest::Method::Post,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling POST /api/v1/down request");
|
||||
return handlePostDown();
|
||||
});
|
||||
|
||||
m_server.route("/api/v1/ping", QHttpServerRequest::Method::Get,
|
||||
[this] {
|
||||
ProxyLogger::getInstance().debug("Handling GET /api/v1/ping request");
|
||||
return handleGetPing();
|
||||
});
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handlePostUp()
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
const bool started = service->startXray();
|
||||
QJsonObject response;
|
||||
response["status"] = started ? "ok" : "error";
|
||||
|
||||
const auto port = started ? extractInboundPort(service->getConfig()) : std::optional<int>{};
|
||||
response["proxyPort"] = proxyPortValue(port);
|
||||
|
||||
if (started) {
|
||||
if (port.has_value()) {
|
||||
ProxyLogger::getInstance().info(QString("Xray process started on port %1").arg(*port));
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Xray started but inbound port is unknown (local proxy owner may be missing)");
|
||||
}
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Failed to start Xray process via HTTP API");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailablePingResponse();
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handlePostDown()
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
const bool stopped = service->stopXray();
|
||||
QJsonObject response;
|
||||
response["status"] = stopped ? "ok" : "error";
|
||||
if (!stopped) {
|
||||
ProxyLogger::getInstance().warning("Failed to stop Xray process via HTTP API");
|
||||
} else {
|
||||
ProxyLogger::getInstance().info("Xray process stopped via HTTP API");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailableStatusResponse();
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpApi::handleGetPing() const
|
||||
{
|
||||
if (auto service = m_service.lock()) {
|
||||
QJsonObject response;
|
||||
response["status"] = "ok";
|
||||
const bool isRunning = service->isXrayRunning();
|
||||
if (isRunning) {
|
||||
const auto port = extractInboundPort(service->getConfig());
|
||||
response["proxyPort"] = proxyPortValue(port);
|
||||
if (port.has_value()) {
|
||||
ProxyLogger::getInstance().debug(QString("Xray port: %1").arg(*port));
|
||||
} else {
|
||||
ProxyLogger::getInstance().warning("Unable to detect inbound port while Xray is running");
|
||||
}
|
||||
} else {
|
||||
response["proxyPort"] = QJsonValue::Null;
|
||||
ProxyLogger::getInstance().debug("Xray is not running");
|
||||
}
|
||||
|
||||
return QHttpServerResponse(response);
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error("Service unavailable: proxy backend is not initialized");
|
||||
return makeServiceUnavailablePingResponse();
|
||||
}
|
||||
32
client/core/local-proxy/httpapi.h
Normal file
32
client/core/local-proxy/httpapi.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerRequest>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#include <QWeakPointer>
|
||||
#include "iproxyservice.h"
|
||||
|
||||
class HttpApi : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HttpApi(QWeakPointer<IProxyService> service, QObject* parent = nullptr);
|
||||
~HttpApi();
|
||||
|
||||
bool start(quint16 port);
|
||||
void stop();
|
||||
|
||||
private:
|
||||
void setupRoutes();
|
||||
|
||||
QHttpServerResponse handlePostUp();
|
||||
QHttpServerResponse handlePostDown();
|
||||
QHttpServerResponse handleGetPing() const;
|
||||
|
||||
QHttpServer m_server;
|
||||
QScopedPointer<QTcpServer> m_tcpServer;
|
||||
QWeakPointer<IProxyService> m_service;
|
||||
};
|
||||
17
client/core/local-proxy/iproxyservice.h
Normal file
17
client/core/local-proxy/iproxyservice.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class IProxyService {
|
||||
public:
|
||||
virtual ~IProxyService() = default;
|
||||
|
||||
virtual QJsonObject getConfig() = 0;
|
||||
|
||||
virtual bool startXray() = 0;
|
||||
virtual bool stopXray() = 0;
|
||||
virtual bool isXrayRunning() const = 0;
|
||||
virtual qint64 getXrayProcessId() const = 0;
|
||||
virtual QString getXrayError() const = 0;
|
||||
};
|
||||
43
client/core/local-proxy/portavailabilityhelper.cpp
Normal file
43
client/core/local-proxy/portavailabilityhelper.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "portavailabilityhelper.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QTcpServer>
|
||||
|
||||
namespace {
|
||||
constexpr int kProxyPortMin = 1024;
|
||||
constexpr int kProxyPortMax = 65535;
|
||||
}
|
||||
|
||||
bool PortAvailabilityHelper::isPortAvailable(int port)
|
||||
{
|
||||
if (port < kProxyPortMin || port > kProxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpServer server;
|
||||
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
|
||||
server.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
std::optional<int> PortAvailabilityHelper::findFirstAvailablePort(int startPort, int endPort)
|
||||
{
|
||||
if (startPort < kProxyPortMin) {
|
||||
startPort = kProxyPortMin;
|
||||
}
|
||||
if (endPort > kProxyPortMax) {
|
||||
endPort = kProxyPortMax;
|
||||
}
|
||||
if (startPort > endPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (int port = startPort; port <= endPort; ++port) {
|
||||
if (isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
11
client/core/local-proxy/portavailabilityhelper.h
Normal file
11
client/core/local-proxy/portavailabilityhelper.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
class PortAvailabilityHelper
|
||||
{
|
||||
public:
|
||||
static bool isPortAvailable(int port);
|
||||
static std::optional<int> findFirstAvailablePort(int startPort, int endPort);
|
||||
};
|
||||
|
||||
133
client/core/local-proxy/proxylogger.cpp
Normal file
133
client/core/local-proxy/proxylogger.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "proxylogger.h"
|
||||
#include <QDir>
|
||||
#include <QTextStream>
|
||||
|
||||
ProxyLogger::ProxyLogger() : m_maxFileSize(0), m_currentLevel(LogLevel::Info)
|
||||
{
|
||||
}
|
||||
|
||||
ProxyLogger::~ProxyLogger()
|
||||
{
|
||||
}
|
||||
|
||||
ProxyLogger& ProxyLogger::getInstance()
|
||||
{
|
||||
static ProxyLogger instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ProxyLogger::init(const QString& logPath, qint64 maxFileSize)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_logPath = logPath;
|
||||
m_maxFileSize = maxFileSize;
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
QDir dir = QFileInfo(m_logPath).dir();
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyLogger::setLogLevel(LogLevel level)
|
||||
{
|
||||
m_currentLevel = level;
|
||||
}
|
||||
|
||||
void ProxyLogger::log(LogLevel level, const QString& message)
|
||||
{
|
||||
logInternal(level, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::debug(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Debug, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::info(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Info, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::warning(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::error(const QString& message)
|
||||
{
|
||||
logInternal(LogLevel::Error, message);
|
||||
}
|
||||
|
||||
void ProxyLogger::logInternal(LogLevel level, const QString& message)
|
||||
{
|
||||
if (m_logPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (level < m_currentLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
checkRotation();
|
||||
|
||||
QFile file(m_logPath);
|
||||
if (!openLogFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
|
||||
stream << QString("[%1] [%2] %3\n").arg(timestamp, levelToString(level), message);
|
||||
stream.flush();
|
||||
file.close();
|
||||
}
|
||||
|
||||
QString ProxyLogger::levelToString(LogLevel level)
|
||||
{
|
||||
switch (level) {
|
||||
case LogLevel::Debug: return "DEBUG";
|
||||
case LogLevel::Info: return "INFO";
|
||||
case LogLevel::Warning: return "WARNING";
|
||||
case LogLevel::Error: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
qint64 ProxyLogger::getCurrentFileSize() const
|
||||
{
|
||||
QFile file(m_logPath);
|
||||
if (file.exists()) {
|
||||
return file.size();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ProxyLogger::checkRotation()
|
||||
{
|
||||
if (m_maxFileSize > 0 && getCurrentFileSize() >= m_maxFileSize) {
|
||||
// Delete the oldest file
|
||||
QFile::remove(QString("%1.%2").arg(m_logPath).arg(MAX_BACKUP_FILES));
|
||||
|
||||
// Shift existing files
|
||||
for (int i = MAX_BACKUP_FILES - 1; i >= 1; --i) {
|
||||
QString oldName = QString("%1.%2").arg(m_logPath).arg(i);
|
||||
QString newName = QString("%1.%2").arg(m_logPath).arg(i + 1);
|
||||
QFile::rename(oldName, newName);
|
||||
}
|
||||
|
||||
// Rename current file
|
||||
QFile::rename(m_logPath, m_logPath + ".1");
|
||||
}
|
||||
}
|
||||
|
||||
bool ProxyLogger::openLogFile(QFile& file)
|
||||
{
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||||
qDebug() << "Failed to open log file:" << m_logPath;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
54
client/core/local-proxy/proxylogger.h
Normal file
54
client/core/local-proxy/proxylogger.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef LOCAL_PROXY_LOGGER_H
|
||||
#define LOCAL_PROXY_LOGGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
#include <QDateTime>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
|
||||
class ProxyLogger
|
||||
{
|
||||
|
||||
public:
|
||||
enum class LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
static ProxyLogger& getInstance();
|
||||
|
||||
void init(const QString& logPath, qint64 maxFileSize = 1024 * 1024 * 10); // 10MB by default
|
||||
void setLogLevel(LogLevel level);
|
||||
|
||||
// Main logging method
|
||||
void log(LogLevel level, const QString& message);
|
||||
|
||||
// Helper methods for convenience
|
||||
void debug(const QString& message);
|
||||
void info(const QString& message);
|
||||
void warning(const QString& message);
|
||||
void error(const QString& message);
|
||||
|
||||
private:
|
||||
ProxyLogger();
|
||||
~ProxyLogger();
|
||||
ProxyLogger(const ProxyLogger&) = delete;
|
||||
ProxyLogger& operator=(const ProxyLogger&) = delete;
|
||||
|
||||
void logInternal(LogLevel level, const QString& message);
|
||||
void checkRotation();
|
||||
QString levelToString(LogLevel level);
|
||||
bool openLogFile(QFile& file);
|
||||
qint64 getCurrentFileSize() const;
|
||||
|
||||
QString m_logPath;
|
||||
qint64 m_maxFileSize;
|
||||
LogLevel m_currentLevel;
|
||||
QMutex m_mutex;
|
||||
static const int MAX_BACKUP_FILES = 3;
|
||||
};
|
||||
|
||||
#endif // LOCAL_PROXY_LOGGER_H
|
||||
114
client/core/local-proxy/proxyserver.cpp
Normal file
114
client/core/local-proxy/proxyserver.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "proxyserver.h"
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ProxyServer::ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_appSettingsRepository(appSettingsRepository)
|
||||
, m_service(new ProxyService(serversRepository, appSettingsRepository, this))
|
||||
{
|
||||
m_lastRestartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
|
||||
}
|
||||
|
||||
ProxyServer::~ProxyServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool ProxyServer::start(quint16 port)
|
||||
{
|
||||
if (m_isRunning) {
|
||||
if (m_currentApiPort == port) {
|
||||
qInfo() << "Local proxy: already running on port" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
qInfo() << "Local proxy: restarting on new port" << port;
|
||||
stop();
|
||||
}
|
||||
|
||||
m_api.reset(new HttpApi(m_service.toWeakRef()));
|
||||
const bool apiStarted = m_api->start(port);
|
||||
if (!apiStarted) {
|
||||
qWarning() << "Local proxy: port is busy:" << port;
|
||||
m_api.reset();
|
||||
m_isRunning = false;
|
||||
m_currentApiPort = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_isRunning = true;
|
||||
m_currentApiPort = port;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProxyServer::stop()
|
||||
{
|
||||
stopXrayProcess();
|
||||
if (m_api) {
|
||||
m_api->stop();
|
||||
m_api.reset();
|
||||
}
|
||||
m_isRunning = false;
|
||||
m_currentApiPort = 0;
|
||||
m_currentProxyPort = 0;
|
||||
}
|
||||
|
||||
bool ProxyServer::startXrayProcess()
|
||||
{
|
||||
return m_service->startXray();
|
||||
}
|
||||
|
||||
void ProxyServer::stopXrayProcess()
|
||||
{
|
||||
m_service->stopXray();
|
||||
}
|
||||
|
||||
bool ProxyServer::syncSettings()
|
||||
{
|
||||
if (!m_isRunning) {
|
||||
qDebug() << "Local proxy: syncSettings called but server is not running";
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint16 newProxyPort = m_appSettingsRepository ? m_appSettingsRepository->localProxyPort() : 0;
|
||||
const int restartToken = m_appSettingsRepository ? m_appSettingsRepository->localProxyRestartToken() : 0;
|
||||
const bool xrayRunning = m_service->isXrayRunning();
|
||||
|
||||
if (!xrayRunning) {
|
||||
qInfo() << "Local proxy: starting Xray on port" << newProxyPort;
|
||||
const bool started = startXrayProcess();
|
||||
if (started) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
if (m_lastRestartToken != restartToken) {
|
||||
qInfo() << "Local proxy: restarting Xray due to config change token";
|
||||
const bool restarted = m_service->restartXray();
|
||||
if (restarted) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return restarted;
|
||||
}
|
||||
|
||||
if (m_currentProxyPort != newProxyPort) {
|
||||
qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort;
|
||||
const bool restarted = m_service->restartXray();
|
||||
if (restarted) {
|
||||
m_currentProxyPort = newProxyPort;
|
||||
m_lastRestartToken = restartToken;
|
||||
}
|
||||
return restarted;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
37
client/core/local-proxy/proxyserver.h
Normal file
37
client/core/local-proxy/proxyserver.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "httpapi.h"
|
||||
#include "proxyservice.h"
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ProxyServer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ProxyServer(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~ProxyServer();
|
||||
|
||||
bool start(quint16 port = 49490);
|
||||
void stop();
|
||||
bool syncSettings();
|
||||
|
||||
private:
|
||||
bool startXrayProcess();
|
||||
void stopXrayProcess();
|
||||
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
QScopedPointer<HttpApi> m_api;
|
||||
QSharedPointer<ProxyService> m_service;
|
||||
bool m_isRunning {false};
|
||||
quint16 m_currentApiPort {0};
|
||||
quint16 m_currentProxyPort {0};
|
||||
int m_lastRestartToken {0};
|
||||
};
|
||||
118
client/core/local-proxy/proxyservice.cpp
Normal file
118
client/core/local-proxy/proxyservice.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "proxyservice.h"
|
||||
|
||||
#include "proxylogger.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void logConfigError(const QString &errorMessage)
|
||||
{
|
||||
if (!errorMessage.isEmpty()) {
|
||||
ProxyLogger::getInstance().error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProxyService::ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_configManager(new ConfigManager(serversRepository, appSettingsRepository))
|
||||
, m_xrayController(new XrayController())
|
||||
{
|
||||
ProxyLogger::getInstance().debug("ProxyService initialized");
|
||||
}
|
||||
|
||||
QJsonObject ProxyService::getConfig()
|
||||
{
|
||||
if (!m_cachedConfig.isEmpty()) {
|
||||
return m_cachedConfig;
|
||||
}
|
||||
|
||||
QString error;
|
||||
const auto configData = m_configManager->buildConfigWithFetch(error);
|
||||
if (!configData) {
|
||||
logConfigError(error);
|
||||
return {};
|
||||
}
|
||||
|
||||
m_cachedConfig = configData->parsedConfig;
|
||||
return m_cachedConfig;
|
||||
}
|
||||
|
||||
bool ProxyService::startXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Starting Xray");
|
||||
|
||||
if (m_xrayController->isXrayRunning()) {
|
||||
ProxyLogger::getInstance().info("Xray is already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
QString error;
|
||||
const auto configData = m_configManager->buildConfigWithFetch(error);
|
||||
if (!configData) {
|
||||
logConfigError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool success = m_xrayController->start(configData->serializedConfig);
|
||||
if (success) {
|
||||
m_cachedConfig = configData->parsedConfig;
|
||||
ProxyLogger::getInstance().info("Xray started successfully");
|
||||
emit xrayStatusChanged(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().error(QStringLiteral("Failed to start Xray: %1").arg(m_xrayController->getError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProxyService::stopXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Stopping Xray");
|
||||
const bool stopped = m_xrayController->stop();
|
||||
if (stopped) {
|
||||
ProxyLogger::getInstance().info("Xray stopped");
|
||||
emit xrayStatusChanged(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().warning(QStringLiteral("Failed to stop Xray: %1").arg(m_xrayController->getError()));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ProxyService::isXrayRunning() const
|
||||
{
|
||||
return m_xrayController->isXrayRunning();
|
||||
}
|
||||
|
||||
qint64 ProxyService::getXrayProcessId() const
|
||||
{
|
||||
return m_xrayController->getProcessId();
|
||||
}
|
||||
|
||||
QString ProxyService::getXrayError() const
|
||||
{
|
||||
return m_xrayController->getError();
|
||||
}
|
||||
|
||||
void ProxyService::clearCache()
|
||||
{
|
||||
m_cachedConfig = QJsonObject();
|
||||
ProxyLogger::getInstance().debug("ProxyService cache cleared");
|
||||
}
|
||||
|
||||
bool ProxyService::restartXray()
|
||||
{
|
||||
ProxyLogger::getInstance().info("Restarting Xray with updated config");
|
||||
clearCache();
|
||||
|
||||
if (m_xrayController->isXrayRunning()) {
|
||||
if (!stopXray()) {
|
||||
ProxyLogger::getInstance().error("Failed to stop Xray during restart, aborting");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
39
client/core/local-proxy/proxyservice.h
Normal file
39
client/core/local-proxy/proxyservice.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "configmanager.h"
|
||||
#include "iproxyservice.h"
|
||||
#include "xraycontroller.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QJsonObject>
|
||||
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
|
||||
class ProxyService : public QObject, public IProxyService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ProxyService(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent = nullptr);
|
||||
~ProxyService() = default;
|
||||
|
||||
QJsonObject getConfig() override;
|
||||
bool startXray() override;
|
||||
bool stopXray() override;
|
||||
bool isXrayRunning() const override;
|
||||
qint64 getXrayProcessId() const override;
|
||||
QString getXrayError() const override;
|
||||
|
||||
void clearCache();
|
||||
bool restartXray();
|
||||
|
||||
signals:
|
||||
void xrayStatusChanged(bool running);
|
||||
|
||||
private:
|
||||
QScopedPointer<ConfigManager> m_configManager;
|
||||
QScopedPointer<XrayController> m_xrayController;
|
||||
QJsonObject m_cachedConfig;
|
||||
};
|
||||
105
client/core/local-proxy/xraycontroller.cpp
Normal file
105
client/core/local-proxy/xraycontroller.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "xraycontroller.h"
|
||||
|
||||
#include "proxylogger.h"
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
namespace {
|
||||
const QString kIpcUnavailableError = QStringLiteral("Failed to communicate with IPC service");
|
||||
}
|
||||
|
||||
XrayController::XrayController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
ProxyLogger::getInstance().debug("XrayController initialized");
|
||||
}
|
||||
|
||||
XrayController::~XrayController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool XrayController::start(const QString &configJson)
|
||||
{
|
||||
if (m_isRunning) {
|
||||
ProxyLogger::getInstance().info("Xray is already running");
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Request to start Xray via IPC");
|
||||
|
||||
m_lastError.clear();
|
||||
|
||||
if (configJson.trimmed().isEmpty()) {
|
||||
m_lastError = QStringLiteral("Config content is empty");
|
||||
ProxyLogger::getInstance().error(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool ipcResult = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(configJson);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
ProxyLogger::getInstance().warning("Failed to start Xray via IPC");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []() {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!ipcResult) {
|
||||
m_lastError = kIpcUnavailableError;
|
||||
ProxyLogger::getInstance().error(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Xray start command sent to IPC service");
|
||||
m_isRunning = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayController::stop()
|
||||
{
|
||||
if (!m_isRunning) {
|
||||
ProxyLogger::getInstance().debug("Skipping Xray stop via IPC: local proxy Xray is not running");
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyLogger::getInstance().info("Stopping Xray via IPC");
|
||||
|
||||
const bool ipcResult = IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStop();
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue()) {
|
||||
ProxyLogger::getInstance().warning("Failed to stop Xray via IPC");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []() {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!ipcResult) {
|
||||
m_lastError = kIpcUnavailableError;
|
||||
ProxyLogger::getInstance().warning(m_lastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_isRunning = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XrayController::isXrayRunning() const
|
||||
{
|
||||
return m_isRunning;
|
||||
}
|
||||
|
||||
qint64 XrayController::getProcessId() const
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString XrayController::getError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
22
client/core/local-proxy/xraycontroller.h
Normal file
22
client/core/local-proxy/xraycontroller.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class XrayController : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XrayController(QObject* parent = nullptr);
|
||||
~XrayController();
|
||||
|
||||
bool start(const QString& configJson);
|
||||
bool stop();
|
||||
bool isXrayRunning() const;
|
||||
qint64 getProcessId() const;
|
||||
QString getError() const;
|
||||
|
||||
private:
|
||||
bool m_isRunning {false};
|
||||
QString m_lastError;
|
||||
};
|
||||
@@ -32,7 +32,7 @@ XrayXPaddingConfig XrayXPaddingConfig::fromJson(const QJsonObject &json)
|
||||
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.key = json.value(configKey::xPaddingKey).toString();
|
||||
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);
|
||||
@@ -365,6 +365,8 @@ XrayServerConfig XrayServerConfig::fromJson(const QJsonObject &json)
|
||||
bool XrayServerConfig::hasEqualServerSettings(const XrayServerConfig &other) const
|
||||
{
|
||||
return port == other.port
|
||||
&& transportProto == other.transportProto
|
||||
&& subnetAddress == other.subnetAddress
|
||||
&& site == other.site
|
||||
&& security == other.security
|
||||
&& flow == other.flow
|
||||
@@ -466,6 +468,17 @@ XrayProtocolConfig XrayProtocolConfig::fromJson(const QJsonObject &json)
|
||||
}
|
||||
}
|
||||
}
|
||||
const QJsonArray outbounds = parsed.value(protocols::xray::outbounds).toArray();
|
||||
if (!outbounds.isEmpty()) {
|
||||
const QJsonObject settings = outbounds[0].toObject().value(protocols::xray::settings).toObject();
|
||||
const QJsonArray vnext = settings.value(protocols::xray::vnext).toArray();
|
||||
if (!vnext.isEmpty()) {
|
||||
const QJsonArray users = vnext[0].toObject().value(protocols::xray::users).toArray();
|
||||
if (!users.isEmpty()) {
|
||||
clientCfg.id = users[0].toObject().value(protocols::xray::id).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
c.clientConfig = clientCfg;
|
||||
} else {
|
||||
c.clientConfig = XrayClientConfig::fromJson(parsed);
|
||||
|
||||
@@ -68,6 +68,8 @@ ErrorCode XrayProtocol::start()
|
||||
{
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
m_teardownDone = false;
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
// Re-uses existing credentials if the config already has them (e.g. imported config).
|
||||
amnezia::serialization::inbounds::InboundCredentials creds;
|
||||
@@ -120,6 +122,17 @@ void XrayProtocol::stop()
|
||||
{
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
// The destructor calls stop() as a safety net, but stop() is also called
|
||||
// explicitly on disconnect. Because stop() spins the event loop via
|
||||
// waitForFinished(), the local proxy may start its own xray on the shared
|
||||
// global engine in between. Running the teardown (xrayStop) twice would then
|
||||
// kill the proxy's just-started xray. Run the engine teardown only once.
|
||||
if (m_teardownDone) {
|
||||
qDebug() << "XrayProtocol::stop() teardown already done, skipping";
|
||||
return;
|
||||
}
|
||||
m_teardownDone = true;
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
|
||||
@@ -38,6 +38,11 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
// Guards against tearing down the shared xray engine more than once.
|
||||
// The destructor also calls stop(); without this the second stop() would
|
||||
// call xrayStop() on the global engine that the local proxy may already own.
|
||||
bool m_teardownDone = false;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
#include "core/utils/routeModes.h"
|
||||
#include "core/utils/commonStructs.h"
|
||||
@@ -440,3 +442,59 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::localProxyOwnerId() const
|
||||
{
|
||||
return value("Conf/localProxyOwnerId", "").toString();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
|
||||
{
|
||||
setValue("Conf/localProxyOwnerId", serverId);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
quint16 SecureAppSettingsRepository::localProxyPort() const
|
||||
{
|
||||
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
|
||||
{
|
||||
setValue("Conf/localProxyPort", port);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return value("Conf/localProxyPortUserDefined", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
|
||||
{
|
||||
setValue("Conf/localProxyPortUserDefined", userDefined);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyHttpEnabled() const
|
||||
{
|
||||
return value("Conf/localProxyHttpEnabled", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyHttpEnabled(bool enabled)
|
||||
{
|
||||
setValue("Conf/localProxyHttpEnabled", enabled);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
int SecureAppSettingsRepository::localProxyRestartToken() const
|
||||
{
|
||||
return value("Conf/localProxyRestartToken", 0).toInt();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::bumpLocalProxyRestartToken()
|
||||
{
|
||||
const int current = localProxyRestartToken();
|
||||
const int next = (current == std::numeric_limits<int>::max()) ? 0 : (current + 1);
|
||||
setValue("Conf/localProxyRestartToken", next);
|
||||
emit localProxySettingsChanged();
|
||||
}
|
||||
|
||||
@@ -93,6 +93,18 @@ public:
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
// Local proxy
|
||||
QString localProxyOwnerId() const;
|
||||
void setLocalProxyOwnerId(const QString &serverId);
|
||||
quint16 localProxyPort() const;
|
||||
void setLocalProxyPort(quint16 port);
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
void setLocalProxyPortUserDefined(bool userDefined);
|
||||
bool isLocalProxyHttpEnabled() const;
|
||||
void setLocalProxyHttpEnabled(bool enabled);
|
||||
int localProxyRestartToken() const;
|
||||
void bumpLocalProxyRestartToken();
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
@@ -106,6 +118,8 @@ signals:
|
||||
void saveLogsChanged(bool enabled);
|
||||
void screenshotsEnabledChanged(bool enabled);
|
||||
void settingsCleared();
|
||||
void localProxySettingsChanged();
|
||||
void localProxyStartFailed(const QString &message);
|
||||
|
||||
private:
|
||||
void setVpnSites(RouteMode mode, const QVariantMap &sites);
|
||||
|
||||
@@ -208,8 +208,8 @@ QString SecureServersRepository::nextAvailableServerName() const
|
||||
int i = 0;
|
||||
QString candidate;
|
||||
do {
|
||||
i++;
|
||||
candidate = QStringLiteral("Server %1").arg(i);
|
||||
++i;
|
||||
candidate = tr("Server") + QLatin1Char(' ') + QString::number(i);
|
||||
} while (usedNames.contains(candidate));
|
||||
|
||||
return candidate;
|
||||
@@ -309,6 +309,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
|
||||
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
if (it == m_serverJsonById.constEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
void removeServer(const QString &serverId);
|
||||
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
|
||||
|
||||
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
|
||||
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
|
||||
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace amnezia
|
||||
XrayServerConfigInvalid = 215,
|
||||
XrayServerNoVlessClients = 216,
|
||||
XrayRealityKeysReadFailed = 217,
|
||||
ServerContainerRuntimeNotSupported = 218,
|
||||
ContainerRuntimeServiceNotRunning = 219,
|
||||
|
||||
// Ssh connection errors
|
||||
SshRequestDeniedError = 300,
|
||||
@@ -124,5 +126,3 @@ namespace amnezia
|
||||
Q_DECLARE_METATYPE(amnezia::ErrorCode)
|
||||
|
||||
#endif // ERRORCODES_H
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::XrayRealityKeysReadFailed):
|
||||
errorMessage = QObject::tr("Server error: failed to read XRay Reality keys from the server");
|
||||
break;
|
||||
case(ErrorCode::ServerContainerRuntimeNotSupported): errorMessage = QObject::tr("Server error: The default container runtime available for installation on this server is not supported.\n Install Docker Engine on the server manually and try again."); break;
|
||||
case(ErrorCode::ContainerRuntimeServiceNotRunning): errorMessage = QObject::tr("Container runtime error: The container runtime service is not running.\n Check the container runtime service on the server, or wait about a minute and try again."); break;
|
||||
|
||||
// Libssh errors
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
|
||||
@@ -25,6 +25,8 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
||||
|
||||
XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
|
||||
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../../../Frameworks @loader_path/../../../../Frameworks"
|
||||
|
||||
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
|
||||
)
|
||||
|
||||
if(DEPLOY)
|
||||
@@ -118,10 +120,20 @@ target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}
|
||||
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
find_package(openvpnadapter REQUIRED)
|
||||
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
|
||||
set_property(TARGET amnezia::openvpnadapter APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
|
||||
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter)
|
||||
|
||||
find_package(awg-apple REQUIRED)
|
||||
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
|
||||
|
||||
find_package(hev-socks5-tunnel REQUIRED)
|
||||
# FIXME(ygurov): https://github.com/conan-io/conan/issues/20034
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
|
||||
set_property(TARGET heiher::hev-socks5-tunnel APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
|
||||
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel)
|
||||
|
||||
@@ -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 sudo -n which $LOCK_CMD > /dev/null 2>&1 || command -v $LOCK_CMD > /dev/null 2>&1; then sudo -n $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,25 +1,34 @@
|
||||
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";\
|
||||
elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
|
||||
else echo "Packet manager not found"; exit 1; fi;\
|
||||
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg";\
|
||||
if pm=$(which apt-get 2>/dev/null || command -v apt-get 2>/dev/null); then silent_inst="-yq install --install-recommends"; what_pkg="-s install"; check_pkgs="-yq update"; docker_pkg="docker.io"; dist="debian";\
|
||||
elif pm=$(which dnf 2>/dev/null || command -v dnf 2>/dev/null); then silent_inst="-yq install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-yq check-update"; docker_pkg="docker"; dist="fedora";\
|
||||
elif pm=$(which yum 2>/dev/null || command -v yum 2>/dev/null); then silent_inst="-y -q install"; what_pkg="--assumeno install --setopt=tsflags=test"; check_pkgs="-y -q check-update"; docker_pkg="docker"; dist="centos";\
|
||||
elif pm=$(which zypper 2>/dev/null || command -v zypper 2>/dev/null); then silent_inst="-nq install"; what_pkg="--dry-run install"; check_pkgs="-nq refresh"; docker_pkg="docker"; dist="suse";\
|
||||
elif pm=$(which pacman 2>/dev/null || command -v pacman 2>/dev/null); then silent_inst="-S --noconfirm --noprogressbar --quiet"; what_pkg="-Sp"; check_pkgs="-Sup"; docker_pkg="docker"; dist="archlinux";\
|
||||
fi;\
|
||||
echo "Dist: $dist, Packet manager: $pm, Install command: $silent_inst, What pkg command: $what_pkg, Check pkgs command: $check_pkgs, Docker pkg: $docker_pkg, Language: $LANG";\
|
||||
echo $LANG | grep -qE '^(en_US.UTF-8|C.UTF-8|C)$' || export LC_ALL=C;\
|
||||
if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\
|
||||
if ! command -v sudo > /dev/null 2>&1; then $pm $check_pkgs; $pm $silent_inst sudo; fi;\
|
||||
if ! command -v fuser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst psmisc; fi;\
|
||||
if ! command -v lsof > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst lsof; fi;\
|
||||
if ! command -v docker > /dev/null 2>&1; then \
|
||||
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
|
||||
sleep 5; sudo systemctl enable --now docker; sleep 5;\
|
||||
if ! sudo -n sh -c 'command -v which > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst which; fi;\
|
||||
if ! sudo -n sh -c 'command -v fuser > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst psmisc; fi;\
|
||||
if ! sudo -n sh -c 'command -v lsof > /dev/null 2>&1'; then sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst lsof; fi;\
|
||||
if ! sudo -n sh -c 'command -v docker > /dev/null 2>&1'; then \
|
||||
sudo -n $pm $check_pkgs;\
|
||||
if ! sudo -n $pm $what_pkg $docker_pkg 2>/dev/null | grep -qi podman; then \
|
||||
sudo -n $pm $silent_inst $docker_pkg;\
|
||||
sleep 5; sudo -n systemctl enable --now docker; sleep 5;\
|
||||
else \
|
||||
echo "Container runtime is not supported";\
|
||||
exit 1;\
|
||||
fi;\
|
||||
fi;\
|
||||
if [ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
|
||||
if ! command -v apparmor_parser > /dev/null 2>&1; then sudo $pm $check_pkgs; sudo $pm $silent_inst apparmor; fi;\
|
||||
if [ "$(sudo -n cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" = "Y" ]; then \
|
||||
if ! sudo -n sh -c 'command -v apparmor_parser > /dev/null 2>&1'; then \
|
||||
sudo -n $pm $check_pkgs; sudo -n $pm $silent_inst apparmor;\
|
||||
fi;\
|
||||
fi;\
|
||||
if [ "$(systemctl is-active docker)" != "active" ]; then \
|
||||
sudo $pm $check_pkgs; sudo $pm $silent_inst $docker_pkg;\
|
||||
sleep 5; sudo systemctl start docker; sleep 5;\
|
||||
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then \
|
||||
sleep 5; sudo -n systemctl start docker; sleep 5;\
|
||||
if [ "$(sudo -n systemctl is-active docker)" != "active" ]; then echo "Container runtime service not running"; fi;\
|
||||
fi;\
|
||||
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
|
||||
docker --version;\
|
||||
sudo -n docker --version || docker --version;\
|
||||
uname -sr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
|
||||
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
|
||||
sudo docker volume ls | grep amnezia | awk '{print $2}' | xargs sudo docker volume rm -f;\
|
||||
sudo docker volume ls --format '{{.Name}}' | grep '^amnezia-' | xargs -r sudo docker volume rm -f;\
|
||||
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
|
||||
sudo rm -frd /opt/amnezia
|
||||
|
||||
@@ -128,6 +128,11 @@ void PageController::showOnStartup()
|
||||
}
|
||||
}
|
||||
|
||||
bool PageController::shouldStartMinimized() const
|
||||
{
|
||||
return m_settingsController->isStartMinimizedEnabled();
|
||||
}
|
||||
|
||||
bool PageController::isTriggeredByConnectButton()
|
||||
{
|
||||
return m_isTriggeredByConnectButton;
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace PageLoader
|
||||
PageSettingsApiDevices,
|
||||
PageSettingsApiSubscriptionKey,
|
||||
PageSettingsKillSwitchExceptions,
|
||||
PageSettingsConnectionType,
|
||||
PageSettingsLocalProxy,
|
||||
|
||||
PageServiceSftpSettings,
|
||||
PageServiceTorWebsiteSettings,
|
||||
@@ -123,6 +125,7 @@ public slots:
|
||||
void updateNavigationBarColor(const int color);
|
||||
|
||||
void showOnStartup();
|
||||
bool shouldStartMinimized() const;
|
||||
|
||||
bool isTriggeredByConnectButton();
|
||||
void setTriggeredByConnectButton(bool trigger);
|
||||
|
||||
@@ -9,6 +9,13 @@ ExportUiController::ExportUiController(ExportController* exportController, QObje
|
||||
: QObject(parent),
|
||||
m_exportController(exportController)
|
||||
{
|
||||
connect(m_exportController, &ExportController::revokeFinished, this, [this](ErrorCode errorCode) {
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
emit revokeConfigFinished();
|
||||
} else {
|
||||
emit exportErrorOccurred(errorCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ExportUiController::generateFullAccessConfig(const QString &serverId)
|
||||
@@ -92,7 +99,6 @@ void ExportUiController::updateClientManagementModel(const QString &serverId, in
|
||||
void ExportUiController::revokeConfig(int row, const QString &serverId, int containerIndex)
|
||||
{
|
||||
m_exportController->revokeConfig(row, serverId, containerIndex);
|
||||
emit revokeConfigFinished();
|
||||
}
|
||||
|
||||
void ExportUiController::renameClient(int row, const QString &clientName, const QString &serverId, int containerIndex)
|
||||
|
||||
@@ -306,14 +306,17 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
|
||||
|
||||
if (asyncUpdate) {
|
||||
emit serverIsBusy(true);
|
||||
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
if (emitBusy)
|
||||
emit serverIsBusy(true);
|
||||
auto *watcher = new QFutureWatcher<ErrorCode>(this);
|
||||
const Proto protocolTypeCopy = protocolType;
|
||||
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
|
||||
[this, watcher, serverId, container, closePage, protocolTypeCopy]() {
|
||||
[this, watcher, serverId, container, closePage, protocolTypeCopy, emitBusy]() {
|
||||
const ErrorCode errorCode = watcher->result();
|
||||
watcher->deleteLater();
|
||||
emit serverIsBusy(false);
|
||||
if (emitBusy)
|
||||
emit serverIsBusy(false);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
const ContainerConfig updatedConfig =
|
||||
|
||||
@@ -37,6 +37,11 @@ SettingsUiController::SettingsUiController(SettingsController* settingsControlle
|
||||
if (m_settingsController->isDevGatewayEnv()) {
|
||||
m_settingsController->enableDevMode();
|
||||
}
|
||||
|
||||
connect(m_settingsController, &SettingsController::localProxySettingsUpdated, this,
|
||||
&SettingsUiController::localProxySettingsUpdated);
|
||||
connect(m_settingsController, &SettingsController::localProxyStartFailed, this,
|
||||
&SettingsUiController::localProxyStartFailed);
|
||||
}
|
||||
|
||||
void SettingsUiController::toggleAmneziaDns(bool enable)
|
||||
@@ -358,3 +363,53 @@ void SettingsUiController::disableHomeAdLabel()
|
||||
m_settingsController->disableHomeAdLabel();
|
||||
emit isHomeAdLabelVisibleChanged(false);
|
||||
}
|
||||
|
||||
bool SettingsUiController::isLocalProxySupported() const
|
||||
{
|
||||
return m_settingsController->isLocalProxySupported();
|
||||
}
|
||||
|
||||
bool SettingsUiController::isLocalProxyHttpEnabled() const
|
||||
{
|
||||
return m_settingsController->isLocalProxyHttpEnabled();
|
||||
}
|
||||
|
||||
int SettingsUiController::localProxyPort() const
|
||||
{
|
||||
return m_settingsController->localProxyPort();
|
||||
}
|
||||
|
||||
QString SettingsUiController::localProxyOwnerId() const
|
||||
{
|
||||
return m_settingsController->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool SettingsUiController::setLocalProxyPort(int port)
|
||||
{
|
||||
return m_settingsController->setLocalProxyPort(port);
|
||||
}
|
||||
|
||||
bool SettingsUiController::isLocalProxyPortBusy(int port) const
|
||||
{
|
||||
return m_settingsController->isLocalProxyPortBusy(port);
|
||||
}
|
||||
|
||||
bool SettingsUiController::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return m_settingsController->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
int SettingsUiController::findFirstAvailableLocalProxyPort(int startPort) const
|
||||
{
|
||||
return m_settingsController->findFirstAvailableLocalProxyPort(startPort);
|
||||
}
|
||||
|
||||
bool SettingsUiController::enableLocalProxy(const QString &ownerId, int port)
|
||||
{
|
||||
return m_settingsController->enableLocalProxy(ownerId, port);
|
||||
}
|
||||
|
||||
void SettingsUiController::disableLocalProxy()
|
||||
{
|
||||
m_settingsController->disableLocalProxy();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ public:
|
||||
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
|
||||
Q_PROPERTY(bool autoStartEnabled READ isAutoStartEnabled NOTIFY autoStartChanged)
|
||||
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
|
||||
Q_PROPERTY(bool isLocalProxySupported READ isLocalProxySupported CONSTANT)
|
||||
Q_PROPERTY(bool isLocalProxyHttpEnabled READ isLocalProxyHttpEnabled NOTIFY localProxySettingsUpdated)
|
||||
Q_PROPERTY(int localProxyPort READ localProxyPort WRITE setLocalProxyPort NOTIFY localProxySettingsUpdated)
|
||||
Q_PROPERTY(QString localProxyOwnerId READ localProxyOwnerId NOTIFY localProxySettingsUpdated)
|
||||
|
||||
public slots:
|
||||
void toggleAmneziaDns(bool enable);
|
||||
@@ -101,6 +105,17 @@ public slots:
|
||||
bool isHomeAdLabelVisible();
|
||||
void disableHomeAdLabel();
|
||||
|
||||
bool isLocalProxySupported() const;
|
||||
bool isLocalProxyHttpEnabled() const;
|
||||
int localProxyPort() const;
|
||||
QString localProxyOwnerId() const;
|
||||
bool setLocalProxyPort(int port);
|
||||
bool isLocalProxyPortBusy(int port) const;
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
int findFirstAvailableLocalProxyPort(int startPort) const;
|
||||
bool enableLocalProxy(const QString &ownerId, int port);
|
||||
void disableLocalProxy();
|
||||
|
||||
signals:
|
||||
void primaryDnsChanged();
|
||||
void secondaryDnsChanged();
|
||||
@@ -135,6 +150,8 @@ signals:
|
||||
void isHomeAdLabelVisibleChanged(bool visible);
|
||||
void autoStartChanged();
|
||||
void startMinimizedChanged();
|
||||
void localProxySettingsUpdated();
|
||||
void localProxyStartFailed(const QString &message);
|
||||
|
||||
private:
|
||||
SettingsController* m_settingsController;
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#include "core/protocols/protocolUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/networkUtilities.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
@@ -272,7 +276,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
|
||||
}
|
||||
|
||||
if (!m_protocolConfig.serverConfig.isThirdPartyConfig) {
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig);
|
||||
applyDefaultsToServerConfig(m_protocolConfig.serverConfig, false);
|
||||
}
|
||||
|
||||
m_originalProtocolConfig = m_protocolConfig;
|
||||
@@ -283,7 +287,7 @@ void XrayConfigModel::updateModel(amnezia::DockerContainer container, const amne
|
||||
}
|
||||
}
|
||||
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config)
|
||||
void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &config, bool fillFlowDefault)
|
||||
{
|
||||
if (config.port.isEmpty()) {
|
||||
config.port = protocols::xray::defaultPort;
|
||||
@@ -306,7 +310,7 @@ void XrayConfigModel::applyDefaultsToServerConfig(amnezia::XrayServerConfig &con
|
||||
config.security = protocols::xray::defaultSecurity;
|
||||
}
|
||||
|
||||
if (config.flow.isEmpty()) {
|
||||
if (fillFlowDefault && config.flow.isEmpty()) {
|
||||
config.flow = protocols::xray::defaultFlow;
|
||||
}
|
||||
|
||||
@@ -585,3 +589,87 @@ QString XrayConfigModel::mkcpDefaultWriteBufferSize()
|
||||
{
|
||||
return QString::fromLatin1(protocols::xray::defaultMkcpWriteBufferSize);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool isValidSingleHost(const QString &t)
|
||||
{
|
||||
if (t.isEmpty() || t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv4Protocol) {
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
QRegExp re = NetworkUtilities::domainRegExp();
|
||||
re.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
return re.exactMatch(t);
|
||||
}
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isValidHost(const QString &host)
|
||||
{
|
||||
const QString t = host.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return isValidSingleHost(t);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isValidSni(const QString &sni)
|
||||
{
|
||||
const QString t = sni.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.startsWith(QLatin1String("*."))) {
|
||||
return isValidSingleHost(t.mid(2));
|
||||
}
|
||||
return isValidSingleHost(t);
|
||||
}
|
||||
|
||||
bool XrayConfigModel::isValidPath(const QString &path)
|
||||
{
|
||||
const QString t = path.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return t.startsWith(QLatin1Char('/'));
|
||||
}
|
||||
|
||||
QStringList XrayConfigModel::validationErrors() const
|
||||
{
|
||||
QStringList errs;
|
||||
const auto &srv = m_protocolConfig.serverConfig;
|
||||
|
||||
if (!srv.port.isEmpty()) {
|
||||
bool ok = false;
|
||||
const int p = srv.port.toInt(&ok);
|
||||
if (!ok || p < 1 || p > 65535) {
|
||||
errs << tr("Port must be in the range of 1 to 65535");
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.security == QLatin1String("tls") || srv.security == QLatin1String("reality")) {
|
||||
if (!isValidSni(srv.sni)) {
|
||||
errs << tr("SNI: enter a valid IP address or domain name");
|
||||
}
|
||||
}
|
||||
|
||||
if (srv.transport == QLatin1String("xhttp")) {
|
||||
if (!isValidHost(srv.xhttp.host)) {
|
||||
errs << tr("Host: enter a valid IP address or domain name");
|
||||
}
|
||||
if (!isValidPath(srv.xhttp.path)) {
|
||||
errs << tr("Path must start with \"/\"");
|
||||
}
|
||||
}
|
||||
|
||||
return errs;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ public:
|
||||
Q_INVOKABLE static QString mkcpDefaultReadBufferSize();
|
||||
Q_INVOKABLE static QString mkcpDefaultWriteBufferSize();
|
||||
|
||||
Q_INVOKABLE static bool isValidHost(const QString &host);
|
||||
Q_INVOKABLE static bool isValidSni(const QString &sni);
|
||||
Q_INVOKABLE static bool isValidPath(const QString &path);
|
||||
Q_INVOKABLE QStringList validationErrors() const;
|
||||
|
||||
public slots:
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::XrayProtocolConfig& protocolConfig);
|
||||
amnezia::XrayProtocolConfig getProtocolConfig();
|
||||
@@ -137,7 +142,7 @@ private:
|
||||
amnezia::XrayProtocolConfig m_protocolConfig;
|
||||
amnezia::XrayProtocolConfig m_originalProtocolConfig;
|
||||
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config);
|
||||
void applyDefaultsToServerConfig(amnezia::XrayServerConfig& config, bool fillFlowDefault = true);
|
||||
};
|
||||
|
||||
#endif // XRAYCONFIGMODEL_H
|
||||
|
||||
@@ -152,4 +152,3 @@ ServerCredentials ServersModel::serverCredentials(int index) const
|
||||
}
|
||||
return m_descriptions.at(index).selfHostedSshCredentials;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
class ServersModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
|
||||
@@ -12,6 +12,7 @@ Item {
|
||||
property int headerTextMaximumLineCount: 2
|
||||
property int headerTextElide: Qt.ElideRight
|
||||
property string descriptionText
|
||||
property string descriptionColor: AmneziaStyle.color.mutedGray
|
||||
property string descriptionLinkText
|
||||
property string descriptionLinkUrl
|
||||
property alias headerRow: headerRow
|
||||
@@ -42,7 +43,7 @@ Item {
|
||||
Layout.topMargin: 16
|
||||
Layout.fillWidth: true
|
||||
text: root.descriptionText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
color: root.descriptionColor
|
||||
visible: root.descriptionText !== ""
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ Item {
|
||||
property int rootButtonTextBottomMargin: 16
|
||||
|
||||
property real drawerHeight: 0.9
|
||||
property bool fitContent: false
|
||||
property Item drawerParent
|
||||
property Component listView
|
||||
|
||||
@@ -219,12 +220,20 @@ Item {
|
||||
parent: drawerParent
|
||||
|
||||
anchors.fill: parent
|
||||
expandedHeight: drawerParent.height * drawerHeight
|
||||
property real measuredContentHeight: 0
|
||||
expandedHeight: (root.fitContent && measuredContentHeight > 0)
|
||||
? Math.min(measuredContentHeight, drawerParent.height * root.drawerHeight)
|
||||
: drawerParent.height * root.drawerHeight
|
||||
|
||||
expandedStateContent: Item {
|
||||
id: container
|
||||
implicitHeight: menu.expandedHeight
|
||||
|
||||
property real fitHeight: backButton.implicitHeight + titleLabel.implicitHeight
|
||||
+ (listViewLoader.item ? listViewLoader.item.contentHeight : 0) + 48
|
||||
onFitHeightChanged: menu.measuredContentHeight = fitHeight
|
||||
Component.onCompleted: menu.measuredContentHeight = fitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
|
||||
@@ -238,6 +247,7 @@ Item {
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
id: titleLabel
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
@@ -12,8 +12,8 @@ import "../Controls2/TextTypes"
|
||||
// MinMaxRowType {
|
||||
// minValue: "0"
|
||||
// maxValue: "0"
|
||||
// onMinChanged: someProperty = val
|
||||
// onMaxChanged: someProperty = val
|
||||
// onMinChanged: function(val) { someProperty = val }
|
||||
// onMaxChanged: function(val) { someProperty = val }
|
||||
// }
|
||||
Item {
|
||||
id: root
|
||||
@@ -21,41 +21,128 @@ Item {
|
||||
property string minValue: "0"
|
||||
property string maxValue: "0"
|
||||
|
||||
property int minLimit: 0
|
||||
property int maxLimit: 2147483647
|
||||
|
||||
property string hintText: root.minLimit > 0
|
||||
? (root.minLimit + "–" + root.maxLimit)
|
||||
: ("≤ " + root.maxLimit)
|
||||
|
||||
signal minChanged(string val)
|
||||
signal maxChanged(string val)
|
||||
signal edited()
|
||||
|
||||
implicitHeight: row.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
implicitHeight: col.implicitHeight
|
||||
implicitWidth: col.implicitWidth
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
function clampValue(text) {
|
||||
if (text === "")
|
||||
return ""
|
||||
var n = parseInt(text, 10)
|
||||
if (isNaN(n))
|
||||
return ""
|
||||
if (n < root.minLimit)
|
||||
n = root.minLimit
|
||||
if (n > root.maxLimit)
|
||||
n = root.maxLimit
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function capEdit(tf, holder) {
|
||||
if (tf.text !== "" && parseInt(tf.text, 10) > root.maxLimit) {
|
||||
tf.text = holder.lastValid
|
||||
tf.cursorPosition = tf.text.length
|
||||
} else {
|
||||
holder.lastValid = tf.text
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
spacing: 4
|
||||
|
||||
// Min field
|
||||
TextFieldWithHeaderType {
|
||||
RowLayout {
|
||||
id: row
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
textField.text: root.minValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.minValue) {
|
||||
root.minChanged(textField.text)
|
||||
spacing: 10
|
||||
|
||||
// Min field
|
||||
TextFieldWithHeaderType {
|
||||
id: minField
|
||||
property string lastValid: ""
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Min")
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onActiveFocusChanged: {
|
||||
if (minField.textField.activeFocus)
|
||||
minField.lastValid = minField.textField.text
|
||||
}
|
||||
textField.onTextEdited: { root.capEdit(minField.textField, minField); root.edited() }
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampValue(minField.textField.text)
|
||||
if (v !== "" && root.maxValue !== "") {
|
||||
var mx = parseInt(root.maxValue, 10)
|
||||
if (!isNaN(mx) && parseInt(v, 10) > mx)
|
||||
root.maxChanged(v)
|
||||
}
|
||||
if (v !== root.minValue)
|
||||
root.minChanged(v)
|
||||
else if (minField.textField.text !== v)
|
||||
minField.textField.text = v
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: minField.textField
|
||||
property: "text"
|
||||
value: root.minValue
|
||||
when: !minField.textField.activeFocus
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
// Max field
|
||||
TextFieldWithHeaderType {
|
||||
id: maxField
|
||||
property string lastValid: ""
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onActiveFocusChanged: {
|
||||
if (maxField.textField.activeFocus)
|
||||
maxField.lastValid = maxField.textField.text
|
||||
}
|
||||
textField.onTextEdited: { root.capEdit(maxField.textField, maxField); root.edited() }
|
||||
textField.onEditingFinished: {
|
||||
var v = root.clampValue(maxField.textField.text)
|
||||
if (v !== "" && root.minValue !== "") {
|
||||
var mn = parseInt(root.minValue, 10)
|
||||
if (!isNaN(mn) && parseInt(v, 10) < mn)
|
||||
v = String(mn)
|
||||
}
|
||||
if (v !== root.maxValue)
|
||||
root.maxChanged(v)
|
||||
else if (maxField.textField.text !== v)
|
||||
maxField.textField.text = v
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: maxField.textField
|
||||
property: "text"
|
||||
value: root.maxValue
|
||||
when: !maxField.textField.activeFocus
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max field
|
||||
TextFieldWithHeaderType {
|
||||
SmallTextType {
|
||||
visible: root.hintText !== ""
|
||||
text: root.hintText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Max")
|
||||
textField.text: root.maxValue
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== root.maxValue) {
|
||||
root.maxChanged(textField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ Item {
|
||||
property string headerTextColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
property alias errorText: errorField.text
|
||||
property bool clearErrorOnTextChanged: true
|
||||
property bool checkEmptyText: false
|
||||
property bool rightButtonClickedOnEnter: false
|
||||
|
||||
@@ -135,7 +136,9 @@ Item {
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
root.errorText = ""
|
||||
if (root.clearErrorOnTextChanged) {
|
||||
root.errorText = ""
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
|
||||
@@ -15,6 +15,8 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool editDirty: false
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -90,6 +92,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: tlsAlpnDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
@@ -133,6 +136,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: tlsFingerprintDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -175,14 +179,21 @@ PageType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: sniFieldTls
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== sni)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== sni) sni = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
sniFieldTls.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name")
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +206,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: realityFingerprintDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
@@ -237,14 +249,21 @@ PageType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: sniFieldReality
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Server Name (SNI)")
|
||||
textField.text: sni
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== sni)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== sni) sni = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== sni) sni = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
sniFieldReality.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name")
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,10 +284,15 @@ PageType {
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var errs = XrayConfigModel.validationErrors()
|
||||
if (errs.length > 0) {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
|
||||
@@ -109,6 +109,7 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
enabled: listView.enabled
|
||||
headerText: qsTr("Port")
|
||||
subtitleText: qsTr("1–65535")
|
||||
|
||||
Binding {
|
||||
target: textFieldWithHeaderType.textField
|
||||
@@ -119,8 +120,8 @@ PageType {
|
||||
}
|
||||
|
||||
textField.maximumLength: 5
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1; top: 65535
|
||||
textField.validator: RegularExpressionValidator {
|
||||
regularExpression: /^(|\d{1,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/
|
||||
}
|
||||
textField.onActiveFocusChanged: {
|
||||
if (textField.activeFocus && textField.text === "" && port !== "") {
|
||||
@@ -131,9 +132,19 @@ PageType {
|
||||
root.portDirty = (textField.text !== port)
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== port) {
|
||||
port = textField.text
|
||||
var v = textFieldWithHeaderType.textField.text
|
||||
if (v !== "") {
|
||||
var n = parseInt(v, 10)
|
||||
if (isNaN(n) || n < 1)
|
||||
n = 1
|
||||
if (n > 65535)
|
||||
n = 65535
|
||||
v = String(n)
|
||||
if (textFieldWithHeaderType.textField.text !== v)
|
||||
textFieldWithHeaderType.textField.text = v
|
||||
}
|
||||
if (v !== port)
|
||||
port = v
|
||||
root.portDirty = false
|
||||
}
|
||||
checkEmptyText: true
|
||||
@@ -198,6 +209,11 @@ PageType {
|
||||
text: qsTr("Save")
|
||||
onClicked: function() {
|
||||
forceActiveFocus()
|
||||
var errs = XrayConfigModel.validationErrors()
|
||||
if (errs.length > 0) {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
|
||||
@@ -15,6 +15,21 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool editDirty: false
|
||||
|
||||
function clampInt(text, lo, hi) {
|
||||
if (text === "")
|
||||
return ""
|
||||
var n = parseInt(text, 10)
|
||||
if (isNaN(n))
|
||||
return ""
|
||||
if (n < lo)
|
||||
n = lo
|
||||
if (n > hi)
|
||||
n = hi
|
||||
return String(n)
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -108,10 +123,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("TTI")
|
||||
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
subtitleText: qsTr("Range 10–100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
|
||||
textField.text: mkcpTti
|
||||
textField.maximumLength: 3
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpTti)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpTti) mkcpTti = textField.text
|
||||
var v = root.clampInt(textField.text, 10, 100)
|
||||
if (v !== mkcpTti) mkcpTti = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +142,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("uplinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity())
|
||||
textField.text: mkcpUplinkCapacity
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpUplinkCapacity)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text
|
||||
var v = root.clampInt(textField.text, 0, 2147483647)
|
||||
if (v !== mkcpUplinkCapacity) mkcpUplinkCapacity = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +161,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("downlinkCapacity")
|
||||
subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity())
|
||||
textField.text: mkcpDownlinkCapacity
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpDownlinkCapacity)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text
|
||||
var v = root.clampInt(textField.text, 0, 2147483647)
|
||||
if (v !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,10 +180,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("readBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
|
||||
textField.text: mkcpReadBufferSize
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text
|
||||
var v = root.clampInt(textField.text, 1, 2147483647)
|
||||
if (v !== mkcpReadBufferSize) mkcpReadBufferSize = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,10 +199,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("writeBufferSize")
|
||||
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
|
||||
textField.text: mkcpWriteBufferSize
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text
|
||||
var v = root.clampInt(textField.text, 1, 2147483647)
|
||||
if (v !== mkcpWriteBufferSize) mkcpWriteBufferSize = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +232,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: modeDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
@@ -239,31 +285,46 @@ PageType {
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: hostField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Host")
|
||||
textField.text: xhttpHost
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpHost) xhttpHost = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xhttpHost) xhttpHost = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
hostField.errorText = XrayConfigModel.isValidHost(v) ? "" : qsTr("Enter a valid IP address or domain name")
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: pathField
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("Path")
|
||||
textField.text: xhttpPath
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpPath)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpPath) xhttpPath = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xhttpPath) xhttpPath = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
pathField.errorText = XrayConfigModel.isValidPath(v) ? "" : qsTr("Path must start with \"/\"")
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: headersDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -307,6 +368,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: uplinkMethodDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -386,6 +448,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: sessionPlacementDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -429,6 +492,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: sessionKeyDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -472,6 +536,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: seqPlacementDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -520,13 +585,19 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("SeqKey")
|
||||
textField.text: xhttpSeqKey
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSeqKey)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xhttpSeqKey) xhttpSeqKey = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: uplinkDataPlacementDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -575,8 +646,13 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkDataKey")
|
||||
textField.text: xhttpUplinkDataKey
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkDataKey)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xhttpUplinkDataKey) xhttpUplinkDataKey = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,12 +673,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("UplinkChunkSize")
|
||||
subtitleText: qsTr("≥ 0 (0 = off)")
|
||||
textField.text: xhttpUplinkChunkSize
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkChunkSize)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text
|
||||
var v = root.clampInt(textField.text, 0, 2147483647)
|
||||
if (v !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,9 +692,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("scMaxBufferedPosts")
|
||||
subtitleText: qsTr("≥ 0")
|
||||
textField.text: xhttpScMaxBufferedPosts
|
||||
textField.maximumLength: 10
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xhttpScMaxBufferedPosts)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text
|
||||
var v = root.clampInt(textField.text, 0, 2147483647)
|
||||
if (v !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,8 +720,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMaxEachPostBytesMin
|
||||
maxValue: xhttpScMaxEachPostBytesMax
|
||||
onMinChanged: xhttpScMaxEachPostBytesMin = val
|
||||
onMaxChanged: xhttpScMaxEachPostBytesMax = val
|
||||
onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
@@ -652,8 +740,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScStreamUpServerSecsMin
|
||||
maxValue: xhttpScStreamUpServerSecsMax
|
||||
onMinChanged: xhttpScStreamUpServerSecsMin = val
|
||||
onMaxChanged: xhttpScStreamUpServerSecsMax = val
|
||||
onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
@@ -671,8 +760,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xhttpScMinPostsIntervalMsMin
|
||||
maxValue: xhttpScMinPostsIntervalMsMax
|
||||
onMinChanged: xhttpScMinPostsIntervalMsMin = val
|
||||
onMaxChanged: xhttpScMinPostsIntervalMsMax = val
|
||||
onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
// ── Padding and multiplexing ──────────────────────────
|
||||
@@ -728,10 +818,15 @@ PageType {
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
var errs = XrayConfigModel.validationErrors()
|
||||
if (errs.length > 0) {
|
||||
PageController.showErrorMessage(errs.join("\n"))
|
||||
return
|
||||
}
|
||||
var headerText = qsTr("Save settings?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
|
||||
@@ -15,6 +15,8 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool editDirty: false
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -61,8 +63,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xPaddingBytesMin
|
||||
maxValue: xPaddingBytesMax
|
||||
onMinChanged: xPaddingBytesMin = val
|
||||
onMaxChanged: xPaddingBytesMax = val
|
||||
onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -81,7 +84,7 @@ PageType {
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
|
||||
@@ -15,6 +15,8 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool editDirty: false
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -78,8 +80,13 @@ PageType {
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("xPaddingKey")
|
||||
textField.text: xPaddingKey
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingKey) xPaddingKey = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xPaddingKey) xPaddingKey = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,13 +97,19 @@ PageType {
|
||||
Layout.topMargin: 8
|
||||
headerText: qsTr("xPaddingHeader")
|
||||
textField.text: xPaddingHeader
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text
|
||||
var v = textField.text.trim()
|
||||
if (v !== xPaddingHeader) xPaddingHeader = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: placementDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -140,6 +153,7 @@ PageType {
|
||||
|
||||
DropDownType {
|
||||
id: methodDropDown
|
||||
fitContent: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
@@ -197,7 +211,7 @@ PageType {
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
|
||||
@@ -15,6 +15,21 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property bool editDirty: false
|
||||
|
||||
function clampSigned(text) {
|
||||
if (text === "" || text === "-")
|
||||
return ""
|
||||
var n = parseInt(text, 10)
|
||||
if (isNaN(n))
|
||||
return ""
|
||||
if (n > 2147483647)
|
||||
n = 2147483647
|
||||
if (n < -2147483648)
|
||||
n = -2147483648
|
||||
return String(n)
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
@@ -78,8 +93,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConcurrencyMin
|
||||
maxValue: xmuxMaxConcurrencyMax
|
||||
onMinChanged: xmuxMaxConcurrencyMin = val
|
||||
onMaxChanged: xmuxMaxConcurrencyMax = val
|
||||
onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
// maxConnections
|
||||
@@ -98,8 +114,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxMaxConnectionsMin
|
||||
maxValue: xmuxMaxConnectionsMax
|
||||
onMinChanged: xmuxMaxConnectionsMin = val
|
||||
onMaxChanged: xmuxMaxConnectionsMax = val
|
||||
onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
// cMaxReuseTimes
|
||||
@@ -118,8 +135,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxCMaxReuseTimesMin
|
||||
maxValue: xmuxCMaxReuseTimesMax
|
||||
onMinChanged: xmuxCMaxReuseTimesMin = val
|
||||
onMaxChanged: xmuxCMaxReuseTimesMax = val
|
||||
onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
// hMaxRequestTimes
|
||||
@@ -138,8 +156,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxRequestTimesMin
|
||||
maxValue: xmuxHMaxRequestTimesMax
|
||||
onMinChanged: xmuxHMaxRequestTimesMin = val
|
||||
onMaxChanged: xmuxHMaxRequestTimesMax = val
|
||||
onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
// hMaxReusableSecs
|
||||
@@ -158,8 +177,9 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
minValue: xmuxHMaxReusableSecsMin
|
||||
maxValue: xmuxHMaxReusableSecsMax
|
||||
onMinChanged: xmuxHMaxReusableSecsMin = val
|
||||
onMaxChanged: xmuxHMaxReusableSecsMax = val
|
||||
onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
|
||||
onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
|
||||
onEdited: root.editDirty = true
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -168,12 +188,16 @@ PageType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
headerText: qsTr("hKeepAlivePeriod")
|
||||
subtitleText: qsTr("Integer, may be negative")
|
||||
textField.text: xmuxHKeepAlivePeriod
|
||||
textField.validator: IntValidator {
|
||||
bottom: 0
|
||||
}
|
||||
textField.maximumLength: 11
|
||||
textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }
|
||||
textField.onTextEdited: root.editDirty = (textField.text !== xmuxHKeepAlivePeriod)
|
||||
textField.onEditingFinished: {
|
||||
if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text
|
||||
var v = root.clampSigned(textField.text)
|
||||
if (v !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = v
|
||||
else if (textField.text !== v) textField.text = v
|
||||
root.editDirty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +218,7 @@ PageType {
|
||||
anchors.rightMargin: 16
|
||||
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges
|
||||
visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
|
||||
enabled: visible
|
||||
text: qsTr("Save")
|
||||
clickedFunc: function () {
|
||||
|
||||
@@ -320,11 +320,27 @@ PageType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: vpnKey
|
||||
id: connectionSwitcher
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: warning.visible ? 16 : 0
|
||||
text: qsTr("Connection")
|
||||
descriptionText: SettingsController.isLocalProxySupported
|
||||
? qsTr("Protocol selection and local proxy setup")
|
||||
: qsTr("Protocol selection")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsConnectionType)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: vpnKey
|
||||
|
||||
Layout.fillWidth: true
|
||||
visible: footer.isVisibleForAmneziaFree
|
||||
|
||||
text: qsTr("Subscription Key")
|
||||
|
||||
98
client/ui/qml/Pages2/PageSettingsConnectionType.qml
Normal file
98
client/ui/qml/Pages2/PageSettingsConnectionType.qml
Normal file
@@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("Connection")
|
||||
}
|
||||
}
|
||||
|
||||
model: 1
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithButtonType {
|
||||
id: vpnProtocolButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("VPN protocol")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsServerProtocols)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: localProxyButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
visible: SettingsController.isLocalProxySupported && ServersUiController.processedServerIsPremium
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
|
||||
text: qsTr("Local proxy")
|
||||
descriptionText: SettingsController.isLocalProxyHttpEnabled ? qsTr("Running: 127.0.0.1:%1").arg(SettingsController.localProxyPort || 0)
|
||||
: qsTr("Off")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsLocalProxy)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: SettingsController.isLocalProxySupported
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
426
client/ui/qml/Pages2/PageSettingsLocalProxy.qml
Normal file
426
client/ui/qml/Pages2/PageSettingsLocalProxy.qml
Normal file
@@ -0,0 +1,426 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
readonly property int localProxyPortMin: 1024
|
||||
readonly property int localProxyPortMax: 65535
|
||||
readonly property int defaultLocalProxyPort: 10808
|
||||
|
||||
property string portValidationError: ""
|
||||
property int pendingStartRequestedPort: -1
|
||||
property int pendingStartAutoSelectedPort: -1
|
||||
property bool pendingStartVpnWasActive: false
|
||||
property bool pendingEnableAfterVpnDisconnect: false
|
||||
property string pendingEnableServerId: ""
|
||||
property int pendingEnableRequestedPort: -1
|
||||
property int pendingEnableAutoSelectedPort: -1
|
||||
property int pendingEnablePortToUse: -1
|
||||
|
||||
function clearPendingEnableAfterVpnDisconnect() {
|
||||
root.pendingEnableAfterVpnDisconnect = false
|
||||
root.pendingEnableServerId = ""
|
||||
root.pendingEnableRequestedPort = -1
|
||||
root.pendingEnableAutoSelectedPort = -1
|
||||
root.pendingEnablePortToUse = -1
|
||||
}
|
||||
|
||||
function enableLocalProxyNow(serverId, requestedPort, autoSelectedPort, portToEnable, vpnWasActive) {
|
||||
if (!SettingsController.enableLocalProxy(serverId, portToEnable)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2).")
|
||||
.arg(root.localProxyPortMin)
|
||||
.arg(root.localProxyPortMax))
|
||||
return false
|
||||
}
|
||||
|
||||
root.pendingStartRequestedPort = requestedPort
|
||||
root.pendingStartAutoSelectedPort = autoSelectedPort
|
||||
root.pendingStartVpnWasActive = vpnWasActive
|
||||
startSuccessToastTimer.restart()
|
||||
return true
|
||||
}
|
||||
|
||||
function getPortField() {
|
||||
var item = listView.itemAtIndex(0)
|
||||
return item !== null ? item.children[0] : null
|
||||
}
|
||||
|
||||
function computePortErrorText() {
|
||||
var portField = getPortField()
|
||||
if (portField === null) return ""
|
||||
const text = portField.textField.text.trim()
|
||||
if (text === "") {
|
||||
return qsTr("Enter a port")
|
||||
}
|
||||
const value = parseInt(text)
|
||||
if (isNaN(value) || value < root.localProxyPortMin || value > root.localProxyPortMax) {
|
||||
return qsTr("Port must be between %1 and %2")
|
||||
.arg(root.localProxyPortMin)
|
||||
.arg(root.localProxyPortMax)
|
||||
}
|
||||
if (SettingsController.isLocalProxyPortBusy(value)) {
|
||||
return qsTr("Port %1 is already in use on this device. Choose another one")
|
||||
.arg(value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function handleLocalProxyToggle(checked) {
|
||||
if (checked) {
|
||||
if (!ServersUiController.processedServerIsPremium) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy is available only for Amnezia Premium"))
|
||||
return
|
||||
}
|
||||
const wasVpnActive = ConnectionController.isConnected || ConnectionController.isConnectionInProgress
|
||||
|
||||
let serverId = ServersUiController.processedServerId
|
||||
if (!serverId) {
|
||||
serverId = ServersUiController.defaultServerId
|
||||
}
|
||||
if (!serverId) {
|
||||
PageController.showNotificationMessage(qsTr("Unable to determine the current server"))
|
||||
return
|
||||
}
|
||||
|
||||
if (SettingsController.isLocalProxyHttpEnabled
|
||||
&& SettingsController.localProxyOwnerId
|
||||
&& SettingsController.localProxyOwnerId !== serverId) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy is already enabled for another server"))
|
||||
return
|
||||
}
|
||||
|
||||
const requestedPort = SettingsController.localProxyPort
|
||||
if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) {
|
||||
PageController.showNotificationMessage(qsTr("Port must be between %1 and %2")
|
||||
.arg(root.localProxyPortMin)
|
||||
.arg(root.localProxyPortMax))
|
||||
return
|
||||
}
|
||||
|
||||
let autoSelectedPort = -1
|
||||
if (SettingsController.isLocalProxyPortBusy(requestedPort)) {
|
||||
if (SettingsController.isLocalProxyPortUserDefined()
|
||||
|| requestedPort !== root.defaultLocalProxyPort) {
|
||||
PageController.showNotificationMessage(qsTr("Port %1 is already in use on this device. Choose another one")
|
||||
.arg(requestedPort))
|
||||
return
|
||||
}
|
||||
|
||||
autoSelectedPort = SettingsController.findFirstAvailableLocalProxyPort(root.defaultLocalProxyPort + 1)
|
||||
if (autoSelectedPort <= 0) {
|
||||
PageController.showNotificationMessage(qsTr("Port %1 is already in use on this device. Choose another one")
|
||||
.arg(requestedPort))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const portToEnable = autoSelectedPort > 0 ? autoSelectedPort : requestedPort
|
||||
if (wasVpnActive) {
|
||||
root.pendingEnableAfterVpnDisconnect = true
|
||||
root.pendingEnableServerId = serverId
|
||||
root.pendingEnableRequestedPort = requestedPort
|
||||
root.pendingEnableAutoSelectedPort = autoSelectedPort
|
||||
root.pendingEnablePortToUse = portToEnable
|
||||
ConnectionController.closeConnection()
|
||||
return
|
||||
}
|
||||
|
||||
root.enableLocalProxyNow(serverId, requestedPort, autoSelectedPort, portToEnable, false)
|
||||
} else {
|
||||
startSuccessToastTimer.stop()
|
||||
root.clearPendingEnableAfterVpnDisconnect()
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartAutoSelectedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
SettingsController.disableLocalProxy()
|
||||
PageController.showNotificationMessage(qsTr("Local proxy stopped"))
|
||||
}
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
HeaderTypeWithSwitcher {
|
||||
id: localProxyHeader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: qsTr("Local Proxy")
|
||||
descriptionText: qsTr("Use a proxy to route selected apps (for example, the CensorTracker extension) through Amnezia Premium.")
|
||||
showSwitcher: ServersUiController.processedServerIsPremium
|
||||
switcher {
|
||||
checked: SettingsController.isLocalProxyHttpEnabled
|
||||
}
|
||||
switcherFunction: function(checked) {
|
||||
// Ignore UI sync toggles; react only to real state change intent.
|
||||
if (checked === SettingsController.isLocalProxyHttpEnabled) {
|
||||
return
|
||||
}
|
||||
root.handleLocalProxyToggle(checked)
|
||||
// Keep checked declaratively linked after any user interaction path.
|
||||
localProxyHeader.switcher.checked = Qt.binding(function() {
|
||||
return SettingsController.isLocalProxyHttpEnabled
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 12
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
color: localProxyHeader.descriptionColor
|
||||
text: qsTr("Only one can be on at a time: VPN or local proxy.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 8
|
||||
Layout.bottomMargin: 28
|
||||
implicitHeight: 32
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
|
||||
text: qsTr("Learn more")
|
||||
clickedFunc: function() {
|
||||
const path = LanguageModel.currentLanguageName === "Русский"
|
||||
? "ru/documentation/instructions/local-proxy"
|
||||
: "documentation/instructions/local-proxy"
|
||||
Qt.openUrlExternally(LanguageModel.getCurrentDocsUrl(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model: 1 // fake model to force the ListView to be created without a model
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
spacing: 16
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: portField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("Address and port")
|
||||
buttonText: qsTr("Copy")
|
||||
errorText: root.portValidationError
|
||||
clearErrorOnTextChanged: false
|
||||
|
||||
enabled: true
|
||||
rightButtonClickedOnEnter: false
|
||||
|
||||
clickedFunc: function() {
|
||||
const portText = portField.effectivePortText()
|
||||
GC.copyToClipBoard("127.0.0.1:" + portText)
|
||||
PageController.showNotificationMessage(qsTr("Copied: 127.0.0.1:%1").arg(portText))
|
||||
}
|
||||
|
||||
textField.validator: RegularExpressionValidator {
|
||||
regularExpression: /^[0-9]{0,5}$/
|
||||
}
|
||||
textField.leftPadding: portPrefix.implicitWidth
|
||||
textField.placeholderText: root.defaultLocalProxyPort.toString()
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
|
||||
|
||||
function syncPortValue() {
|
||||
const port = SettingsController.localProxyPort
|
||||
const isValidPort = port >= root.localProxyPortMin && port <= root.localProxyPortMax
|
||||
textField.text = isValidPort ? port.toString() : ""
|
||||
}
|
||||
|
||||
function portValue() {
|
||||
const value = parseInt(textField.text)
|
||||
return isNaN(value) ? -1 : value
|
||||
}
|
||||
|
||||
function effectivePortText() {
|
||||
const value = portValue()
|
||||
if (value >= root.localProxyPortMin && value <= root.localProxyPortMax) {
|
||||
return value.toString()
|
||||
}
|
||||
const fallback = SettingsController.localProxyPort
|
||||
if (fallback >= root.localProxyPortMin && fallback <= root.localProxyPortMax) {
|
||||
return fallback.toString()
|
||||
}
|
||||
return root.defaultLocalProxyPort.toString()
|
||||
}
|
||||
|
||||
Component.onCompleted: syncPortValue()
|
||||
|
||||
textField.onTextChanged: {
|
||||
if (textField.activeFocus) {
|
||||
root.portValidationError = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: portPrefix
|
||||
|
||||
parent: portField.textField
|
||||
text: "127.0.0.1:"
|
||||
color: AmneziaStyle.color.paleGray
|
||||
font.pixelSize: portField.textField.font.pixelSize
|
||||
font.weight: portField.textField.font.weight
|
||||
font.family: portField.textField.font.family
|
||||
z: 1
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Save")
|
||||
enabled: true
|
||||
|
||||
clickedFunc: function() {
|
||||
if (SettingsController.isLocalProxyHttpEnabled) {
|
||||
PageController.showNotificationMessage(qsTr("Disable Local Proxy to change the port"))
|
||||
return
|
||||
}
|
||||
const validationError = root.computePortErrorText()
|
||||
root.portValidationError = validationError
|
||||
if (validationError !== "") {
|
||||
return
|
||||
}
|
||||
|
||||
const value = portField.portValue()
|
||||
if (!SettingsController.setLocalProxyPort(value)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to save port. Valid range: %1-%2")
|
||||
.arg(root.localProxyPortMin)
|
||||
.arg(root.localProxyPortMax))
|
||||
} else {
|
||||
PageController.showNotificationMessage(qsTr("Port saved: %1").arg(value))
|
||||
}
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: startSuccessToastTimer
|
||||
interval: 250
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (!SettingsController.isLocalProxyHttpEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (root.pendingStartAutoSelectedPort > 0) {
|
||||
PageController.showNotificationMessage(qsTr("Port %1 is in use — selected free port %2.")
|
||||
.arg(root.defaultLocalProxyPort)
|
||||
.arg(root.pendingStartAutoSelectedPort))
|
||||
} else if (root.pendingStartVpnWasActive && root.pendingStartRequestedPort > 0) {
|
||||
PageController.showNotificationMessage(qsTr("VPN turned off. Local proxy is running: 127.0.0.1:%1")
|
||||
.arg(root.pendingStartRequestedPort))
|
||||
} else if (root.pendingStartRequestedPort > 0) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy is running: 127.0.0.1:%1")
|
||||
.arg(root.pendingStartRequestedPort))
|
||||
}
|
||||
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartAutoSelectedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ConnectionController
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (!root.pendingEnableAfterVpnDisconnect) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ConnectionController.isConnected || ConnectionController.isConnectionInProgress) {
|
||||
return
|
||||
}
|
||||
|
||||
const serverId = root.pendingEnableServerId
|
||||
const requestedPort = root.pendingEnableRequestedPort
|
||||
const autoSelectedPort = root.pendingEnableAutoSelectedPort
|
||||
const portToEnable = root.pendingEnablePortToUse
|
||||
root.clearPendingEnableAfterVpnDisconnect()
|
||||
|
||||
root.enableLocalProxyNow(serverId, requestedPort, autoSelectedPort, portToEnable, true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsController
|
||||
|
||||
function onLocalProxySettingsUpdated() {
|
||||
var portField = root.getPortField()
|
||||
if (portField !== null && !portField.textField.activeFocus) {
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
|
||||
function onLocalProxyStartFailed(message) {
|
||||
startSuccessToastTimer.stop()
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartAutoSelectedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
PageController.showNotificationMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersUiController
|
||||
|
||||
function onProcessedServerIdChanged() {
|
||||
var portField = root.getPortField()
|
||||
if (portField !== null && !portField.textField.activeFocus) {
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ PageType {
|
||||
}
|
||||
|
||||
function onExportErrorOccurred(error) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ Window {
|
||||
}
|
||||
}
|
||||
|
||||
visible: true
|
||||
visible: !GC.isDesktop()
|
||||
width: GC.screenWidth
|
||||
height: GC.screenHeight
|
||||
minimumWidth: GC.isDesktop() ? 360 : 0
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
<file>Pages2/PageSettingsDns.qml</file>
|
||||
<file>Pages2/PageSettingsKillSwitch.qml</file>
|
||||
<file>Pages2/PageSettingsKillSwitchExceptions.qml</file>
|
||||
<file>Pages2/PageSettingsConnectionType.qml</file>
|
||||
<file>Pages2/PageSettingsLocalProxy.qml</file>
|
||||
<file>Pages2/PageSettingsLogging.qml</file>
|
||||
<file>Pages2/PageSettingsServerData.qml</file>
|
||||
<file>Pages2/PageSettingsServerInfo.qml</file>
|
||||
|
||||
@@ -19,12 +19,12 @@ class AmneziaVPN(ConanFile):
|
||||
|
||||
if has_service:
|
||||
if os == "Windows":
|
||||
self.requires("awg-windows/0.1.8")
|
||||
self.requires("awg-windows/0.1.9")
|
||||
self.requires("tap-windows6/9.27.0")
|
||||
self.requires("win-split-tunnel/1.2.5.0")
|
||||
self.requires("wintun/0.14.1")
|
||||
else:
|
||||
self.requires("awg-go/0.2.16")
|
||||
self.requires("awg-go/0.2.18")
|
||||
|
||||
self.requires("amnezia-xray-bindings/1.1.0")
|
||||
self.requires("tun2socks/2.6.0")
|
||||
@@ -32,13 +32,13 @@ class AmneziaVPN(ConanFile):
|
||||
self.requires("v2ray-rules-dat/202603162227")
|
||||
|
||||
if has_ne:
|
||||
self.requires("awg-apple/2.0.1")
|
||||
self.requires("awg-apple/2.0.2")
|
||||
self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True})
|
||||
self.requires("openvpnadapter/1.0.0")
|
||||
|
||||
if os == "Android":
|
||||
self.requires("amnezia-libxray/1.0.0")
|
||||
self.requires("awg-android/1.1.7")
|
||||
self.requires("awg-android/2.0.1")
|
||||
self.requires("openvpn-pt-android/1.0.0")
|
||||
|
||||
# expicitly use libssh@amnezia to prevent it from being downloaded from conan-center
|
||||
|
||||
@@ -9,7 +9,7 @@ import platform
|
||||
|
||||
class AwgAndroid(ConanFile):
|
||||
name = "awg-android"
|
||||
version = "1.1.7"
|
||||
version = "2.0.1"
|
||||
settings = "os", "arch", "build_type", "compiler"
|
||||
|
||||
def configure(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
|
||||
class AwgApple(ConanFile):
|
||||
name = "awg-apple"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
settings = "os", "arch", "compiler"
|
||||
|
||||
@property
|
||||
@@ -39,7 +39,7 @@ class AwgApple(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-apple/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="9fe4f8cfbb6a751558b54b7979db3a5ea46e49731912aae99f093e84a1433e97", strip_root=True
|
||||
sha256="a04f49eac9f82bbf5dd9031bab188d44de2b3482efde1b6e970821de1d5a3c5d", strip_root=True
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
|
||||
class AwgGo(ConanFile):
|
||||
name = "awg-go"
|
||||
version = "0.2.16"
|
||||
version = "0.2.18"
|
||||
package_type = "application"
|
||||
settings = "os", "arch"
|
||||
|
||||
@@ -42,7 +42,7 @@ class AwgGo(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="34da7d4189f215f3930de441548bc2a0c89d54d347a4fb85cb9c715fce6413aa", strip_root=True
|
||||
sha256="58eefbd012e79bd1525f0e02d748979e9480acc1a339df8ceb3b9ffafcedb1ba", strip_root=True
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
|
||||
class AwgWindows(ConanFile):
|
||||
name = "awg-windows"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
settings = "os", "arch"
|
||||
|
||||
@property
|
||||
@@ -63,7 +63,7 @@ class AwgWindows(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-windows/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="1de472832b332515c96cdf14ea887edde42ed7ad173675280c51baa9a3ef62f2", strip_root=True)
|
||||
sha256="5c29a75cb2beae291cc51b64840a39f838da5f300b9e956f7964813a687ec74c", strip_root=True)
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
|
||||
@@ -650,6 +650,9 @@ class OpenSSLConan(ConanFile):
|
||||
if self._use_nmake:
|
||||
self.cpp_info.components["ssl"].libs = ["libssl"]
|
||||
self.cpp_info.components["crypto"].libs = ["libcrypto"]
|
||||
elif self.settings.os == "Android" and self.options.shared:
|
||||
self.cpp_info.components["ssl"].libs = ["ssl_3"]
|
||||
self.cpp_info.components["crypto"].libs = ["crypto_3"]
|
||||
else:
|
||||
self.cpp_info.components["ssl"].libs = ["ssl"]
|
||||
self.cpp_info.components["crypto"].libs = ["crypto"]
|
||||
|
||||
@@ -28,7 +28,7 @@ class Openvpn(ConanFile):
|
||||
|
||||
def build_requirements(self):
|
||||
if self._is_windows:
|
||||
self.tool_requires("cmake/[>=3.14 <4]")
|
||||
self.tool_requires("cmake/[>=4.2]")
|
||||
else:
|
||||
self.tool_requires("libtool/2.4.7")
|
||||
self.tool_requires("automake/1.16.5")
|
||||
|
||||
Reference in New Issue
Block a user