Compare commits

...

36 Commits

Author SHA1 Message Date
aiamnezia
24f545d001 Merge branch 'dev' into feature/local-proxy-integration 2026-04-21 16:14:49 +04:00
aiamnezia
2b0d86c626 fix: waiting starting xray before setting running flag 2026-04-13 11:04:19 +04:00
aiamnezia
fe773a108e fix: waiting VPN disconnect before proxy turning on 2026-04-13 10:19:01 +04:00
aiamnezia
a0997156f6 fix: restoring server_uuid within config update 2026-04-13 09:40:23 +04:00
aiamnezia
9a6e975622 fix: stop xray before closing the app 2026-04-13 09:24:55 +04:00
aiamnezia
78f09634a4 fix: prevent stopping Xray due to changing local proxy port
- Added a check in XrayController::stop() to skip the stop operation if Xray is not currently running, with a debug log for clarity.
2026-04-13 07:31:26 +04:00
aiamnezia
be692001b0 fix: fix local proxy settings and restart logic
- Added local proxy restart token management in Settings.
- Implemented logic to handle local proxy state on application quit.
- Updated ProxyServer to restart based on configuration changes.
- Enhanced API configuration updates to bump restart token when necessary.
- Improved UI components to reflect local proxy availability and state.
- Added new error handling and notifications for local proxy operations.
2026-04-13 07:14:42 +04:00
aiamnezia
850b8ea03b Merge branch 'dev' into feature/local-proxy-integration 2026-04-13 03:07:45 +04:00
aiamnezia
c645d07a87 chore: fix build 2026-03-24 16:39:59 +04:00
aiamnezia
7f8786720e Merge branch 'dev' into feature/local-proxy-integration 2026-03-24 15:48:49 +04:00
aiamnezia
25c70b5bf6 fix: fix android build 2026-02-13 17:21:15 +04:00
aiamnezia
7bf16f075c feat: add mutual exclusion between local proxy and vpn 2026-02-13 17:07:35 +04:00
aiamnezia
6518d4866e fix: Fix local proxy UI 2026-02-13 16:46:49 +04:00
aiamnezia
4c2010244b feat: Update local proxy settings page 2026-01-27 15:09:02 +04:00
aiamnezia
e946ee2430 feat: enhance ConfigManager with dynamic proxy port resolution and availability check
- Added functionality to resolve and validate the local proxy port within a specified range.
- Implemented a method to check if a port is available before applying it to the configuration.
- Updated ProxyService to handle the new port resolution logic and cache the parsed configuration.
2026-01-27 12:45:38 +04:00
aiamnezia
5fab8363e7 feat: made API port static and proxy port configurable 2026-01-27 11:09:27 +04:00
aiamnezia
35c2e1564b chore: add debug 2026-01-23 15:33:25 +04:00
aiamnezia
ca5bca085b feat: remove logs for local proxy 2026-01-19 16:25:59 +04:00
aiamnezia
33b2a3d2fc Merge branch 'dev' into feature/local-proxy-integration 2026-01-16 17:48:48 +04:00
aiamnezia
917f5858a8 fix: update ProxyService to use new configuration fetching method 2026-01-15 23:35:39 +04:00
aiamnezia
e98c11079a feat: enhance ConfigManager with fetch capabilities for Xray configuration 2026-01-15 23:05:36 +04:00
aiamnezia
84d908d4d8 feat: enhance local proxy error handling and configuration management
- Added signal for local proxy start failure to Settings.
- Updated CoreController to emit failure signals with descriptive messages when local proxy fails to start.
- Refactored XrayController to accept JSON configuration directly, improving configuration handling.
- Removed unused config file loading logic to streamline the XrayController's functionality.
2026-01-15 21:04:10 +04:00
aiamnezia
412e69af9b feat: update ConfigManager and ProxyServer to utilize Settings
- Modified ConfigManager to accept a Settings object for improved configuration management.
- Updated ProxyServer to initialize with Settings, enhancing dependency injection.
2025-12-31 21:16:45 +04:00
aiamnezia
4492b0af7e fix: fix local proxy page 2025-12-31 20:15:40 +04:00
aiamnezia
806b1d75af refactor: streamline HTTP API for Xray control
- Removed outdated config management routes and consolidated Xray control endpoints.
- Updated response structures to ensure consistent error handling across API calls.
2025-12-31 19:30:29 +04:00
aiamnezia
9740e7557a feat: add dynamic local proxy lifecycle in depence of settings 2025-12-31 18:48:57 +04:00
aiamnezia
5ab82e2196 chore: fix android build 2025-12-30 16:59:15 +04:00
aiamnezia
ae44de7101 chore: fix win build 2025-12-30 16:05:05 +04:00
aiamnezia
2be6079e21 refactor: enhance XrayController to use IPC for process management
- Replaced direct process management with IPC calls for starting and stopping the Xray process.
- Improved error handling for IPC communication and config file loading.
- Removed unused methods and variables related to direct process handling.
- Updated logging to reflect changes in process management.
2025-12-30 15:33:59 +04:00
aiamnezia
2a653f8876 feat: implement local proxy settings UI and functionality
- Added new QML pages for managing local proxy settings and connection types.
- Updated SettingsController to handle local proxy enablement and port configuration.
- Enhanced server model to include processed server UUID for local proxy management.
2025-12-30 14:19:32 +04:00
aiamnezia
41ab51a5ef feat: add server UUID management and local proxy settings
- Implemented UUID migration for servers to ensure each server has a unique identifier.
- Added methods for managing local proxy settings, including owner UUID, port, and HTTP enablement.
- Updated server model to include server UUID role for better data handling.
2025-12-30 11:02:59 +04:00
aiamnezia
300558c33c Merge branch 'dev' into feature/local-proxy-integration 2025-12-19 14:18:26 +04:00
aiamnezia
774deb87f5 fix build scripts 2025-08-08 09:44:50 +04:00
aiamnezia
bae7fbd222 Add qtwebsockets in deploy.yml 2025-08-08 09:14:35 +04:00
aiamnezia
97e4b95673 Add qthttpserver deploy.yml 2025-08-08 09:07:28 +04:00
aiamnezia
2ae97c5cda feat: Added local proxy server 2025-08-08 06:44:18 +04:00
38 changed files with 2595 additions and 241 deletions

View File

@@ -31,7 +31,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'
@@ -129,7 +129,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'
@@ -222,7 +222,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'
@@ -234,7 +234,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'
@@ -337,7 +337,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'
@@ -414,7 +414,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'
@@ -542,7 +542,7 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.10.1
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 }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}

3
.gitignore vendored
View File

@@ -10,7 +10,8 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.cursorignore
.cursor/
# Qt-es
/.qmake.cache

View File

@@ -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
@@ -46,6 +50,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()

View File

@@ -124,6 +124,11 @@ file(GLOB_RECURSE PAGE_LOGIC_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/ui/pages_l
file(GLOB CONFIGURATORS_H CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.h)
file(GLOB CONFIGURATORS_CPP CONFIGURE_DEPENDS ${CLIENT_ROOT_DIR}/configurators/*.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
@@ -161,6 +166,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}/protocols/ikev2_vpn_protocol_windows.h

View File

@@ -1,7 +1,10 @@
#include "coreController.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QDebug>
#include <QTranslator>
#include <QStandardPaths>
#if defined(Q_OS_ANDROID)
#include "core/installedAppsImageProvider.h"
@@ -26,10 +29,61 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initNotificationHandler();
initLocalProxy();
m_translator.reset(new QTranslator());
updateTranslator(m_settings->getAppLanguage());
}
void CoreController::initLocalProxy()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
constexpr quint16 kLocalProxyApiPort = 49490;
m_proxyServer.reset(new ProxyServer(m_settings, this));
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
if (m_settings && m_settings->isLocalProxyHttpEnabled()) {
m_settings->setLocalProxyHttpEnabled(false);
}
});
auto syncLocalProxy = [this]() {
if (!m_proxyServer) {
return;
}
const bool httpEnabled = m_settings->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_settings->setLocalProxyHttpEnabled(false);
emit m_settings->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_settings->setLocalProxyHttpEnabled(false);
emit m_settings->localProxyStartFailed(tr("Couldnt 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_settings.get(), &Settings::localProxySettingsChanged, this, syncLocalProxy);
#endif
}
void CoreController::initModels()
{
m_containersModel.reset(new ContainersModel(this));
@@ -118,6 +172,8 @@ void CoreController::initControllers()
m_pageController.reset(new PageController(m_serversModel, m_settings));
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
connect(m_connectionController.get(), &ConnectionController::localProxyStoppedBecauseVpnTurnedOn, m_pageController.get(),
&PageController::showNotificationMessage);
m_focusController.reset(new FocusController(m_engine, this));
m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get());

View File

@@ -52,7 +52,8 @@
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h"
#include "core/local-proxy/proxyserver.h"
#include "ui/notificationhandler.h"
#endif
class CoreController : public QObject
@@ -79,6 +80,7 @@ private:
void initAndroidController();
void initAppleController();
void initSignalHandlers();
void initLocalProxy();
void initNotificationHandler();
@@ -152,6 +154,10 @@ private:
#endif
QScopedPointer<SftpConfigModel> m_sftpConfigModel;
QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
QScopedPointer<ProxyServer> m_proxyServer;
#endif
};
#endif // CORECONTROLLER_H

View File

@@ -0,0 +1,466 @@
#include "configmanager.h"
#include "containers/containers_defs.h"
#include "core/api/apiDefs.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/defs.h"
#include "portavailabilityhelper.h"
#include "proxylogger.h"
#include "settings.h"
#include "version.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QSaveFile>
#include <QSysInfo>
#include <QStandardPaths>
#include <QUuid>
ConfigManager::ConfigManager(const std::shared_ptr<Settings> &settings)
: m_settings(settings)
{
ProxyLogger::getInstance().debug("ConfigManager initialized (Settings-backed)");
}
namespace {
namespace gateway_key {
constexpr char apiConfig[] = "api_config";
constexpr char authData[] = "auth_data";
constexpr char userCountryCode[] = "user_country_code";
constexpr char serviceType[] = "service_type";
constexpr char serviceProtocol[] = "service_protocol";
constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version";
constexpr char publicKey[] = "public_key";
constexpr char vless[] = "vless";
} // namespace gateway_key
constexpr quint16 kDefaultProxyPort = 10808;
constexpr int kProxyPortMin = 1024;
constexpr int kProxyPortMax = 65535;
int resolveProxyPort(const std::shared_ptr<Settings> &settings)
{
if (!settings) {
return kDefaultProxyPort;
}
const quint16 port = settings->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_settings) {
const QString message = QStringLiteral("Settings backend is not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerUuid = m_settings->localProxyOwnerUuid();
if (ownerUuid.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner UUID is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = findServerByUuid(ownerUuid);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with UUID %1 not found in Settings").arg(ownerUuid);
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(amnezia::config_key::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(amnezia::config_key::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.ownerUuid = ownerUuid;
data.serverName = ownerServer->value(amnezia::config_key::name).toString();
data.parsedConfig = doc.object();
const int proxyPort = resolveProxyPort(m_settings);
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_settings) {
const QString message = QStringLiteral("Settings backend is not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QString ownerUuid = m_settings->localProxyOwnerUuid();
if (ownerUuid.isEmpty()) {
const QString message = QStringLiteral("Local proxy owner UUID is not configured");
ProxyLogger::getInstance().warning(message);
errorDescription = message;
return std::nullopt;
}
const auto ownerServer = findServerByUuid(ownerUuid);
if (!ownerServer) {
const QString message = QStringLiteral("Owner server with UUID %1 not found in Settings").arg(ownerUuid);
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(amnezia::config_key::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.ownerUuid = ownerUuid;
data.serverName = ownerServer->value(amnezia::config_key::name).toString();
data.parsedConfig = doc.object();
int selectedPort = resolveProxyPort(m_settings);
const bool isUserDefinedPort = m_settings->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<QJsonObject> ConfigManager::findServerByUuid(const QString &uuid) const
{
if (!m_settings) {
return std::nullopt;
}
const QJsonArray servers = m_settings->serversArray();
for (const QJsonValue &value : servers) {
const QJsonObject server = value.toObject();
if (server.value(amnezia::config_key::server_uuid).toString() == uuid) {
return server;
}
}
return std::nullopt;
}
std::optional<QString> ConfigManager::extractSerializedXrayConfig(const QJsonObject &server) const
{
const QJsonArray containers = server.value(amnezia::config_key::containers).toArray();
const QString targetContainer = ContainerProps::containerToString(amnezia::DockerContainer::Xray);
const QString protoKey = ProtocolProps::protoToString(amnezia::Proto::Xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(amnezia::config_key::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(protoKey).toObject();
const QString serialized = proto.value(amnezia::config_key::last_config).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
std::optional<QString> ConfigManager::fetchSerializedXrayConfigFromGateway(const QJsonObject &server, QString &errorDescription) const
{
errorDescription.clear();
if (!m_settings) {
const QString message = QStringLiteral("Settings backend is not available");
ProxyLogger::getInstance().error(message);
errorDescription = message;
return std::nullopt;
}
const QJsonObject apiConfig = server.value(gateway_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(gateway_key::userCountryCode).toString();
const QString serviceType = apiConfig.value(gateway_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[gateway_key::osVersion] = QSysInfo::productType();
apiPayload[gateway_key::appVersion] = QString(APP_VERSION);
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
if (!appLanguage.isEmpty()) {
apiPayload[apiDefs::key::appLanguage] = appLanguage;
}
apiPayload[gateway_key::uuid] = m_settings->getInstallationUuid(true);
apiPayload[gateway_key::userCountryCode] = userCountryCode;
apiPayload[gateway_key::serviceType] = serviceType;
apiPayload[gateway_key::serviceProtocol] = gateway_key::vless;
apiPayload[gateway_key::publicKey] = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QJsonObject authData = server.value(gateway_key::authData).toObject();
if (!authData.isEmpty()) {
apiPayload[gateway_key::authData] = authData;
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->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(amnezia::config_key::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"));
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <memory>
#include <optional>
#include <QJsonObject>
#include <QString>
class Settings;
class ConfigManager {
public:
struct ConfigData {
QString ownerUuid;
QString serverName;
QString serializedConfig;
QJsonObject parsedConfig;
};
explicit ConfigManager(const std::shared_ptr<Settings> &settings);
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<QJsonObject> findServerByUuid(const QString &uuid) const;
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;
std::shared_ptr<Settings> m_settings;
};

View 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();
}

View 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;
};

View 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;
};

View 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;
}

View 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);
};

View 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;
}

View 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

View File

@@ -0,0 +1,111 @@
#include "proxyserver.h"
#include "settings.h"
#include <QDebug>
ProxyServer::ProxyServer(const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent)
, m_settings(settings)
, m_service(new ProxyService(settings, this))
{
m_lastRestartToken = m_settings ? m_settings->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_settings ? m_settings->localProxyPort() : 0;
const int restartToken = m_settings ? m_settings->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;
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <QObject>
#include <QScopedPointer>
#include <QSharedPointer>
#include <memory>
#include "httpapi.h"
#include "proxyservice.h"
class Settings;
class ProxyServer : public QObject
{
Q_OBJECT
public:
explicit ProxyServer(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
~ProxyServer();
bool start(quint16 port = 49490);
void stop();
bool syncSettings();
private:
bool startXrayProcess();
void stopXrayProcess();
std::shared_ptr<Settings> m_settings;
QScopedPointer<HttpApi> m_api;
QSharedPointer<ProxyService> m_service;
bool m_isRunning {false};
quint16 m_currentApiPort {0};
quint16 m_currentProxyPort {0};
int m_lastRestartToken {0};
};

View File

@@ -0,0 +1,117 @@
#include "proxyservice.h"
#include "proxylogger.h"
namespace {
void logConfigError(const QString &errorMessage)
{
if (!errorMessage.isEmpty()) {
ProxyLogger::getInstance().error(errorMessage);
}
}
} // namespace
ProxyService::ProxyService(const std::shared_ptr<Settings> &settings, QObject* parent)
: QObject(parent)
, m_configManager(new ConfigManager(settings))
, 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();
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "configmanager.h"
#include "iproxyservice.h"
#include "xraycontroller.h"
#include <QObject>
#include <QScopedPointer>
#include <QJsonObject>
#include <memory>
class Settings;
class ProxyService : public QObject, public IProxyService {
Q_OBJECT
public:
explicit ProxyService(const std::shared_ptr<Settings> &settings, 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;
};

View File

@@ -0,0 +1,105 @@
#include "xraycontroller.h"
#include "proxylogger.h"
#include "core/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;
}

View 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;
};

View File

@@ -114,6 +114,8 @@ namespace amnezia
constexpr char nameOverriddenByUser[] = "nameOverriddenByUser";
constexpr char server_uuid[] = "server_uuid";
}
namespace protocols

View File

@@ -262,6 +262,8 @@
<file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
<file>ui/qml/Pages2/PageSettingsConnectionType.qml</file>
<file>ui/qml/Pages2/PageSettingsLocalProxy.qml</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>

View File

@@ -3,11 +3,14 @@
#include "QCoreApplication"
#include "QThread"
#include <limits>
#include "core/networkUtilities.h"
#include "version.h"
#include "containers/containers_defs.h"
#include "logger.h"
#include <QUuid>
namespace
{
@@ -43,6 +46,8 @@ Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_N
}
}
migrateServerUuids();
m_gatewayEndpoint = gatewayEndpoint;
}
@@ -62,8 +67,16 @@ QJsonObject Settings::server(int index) const
void Settings::addServer(const QJsonObject &server)
{
QJsonObject serverWithUuid = server;
if (!serverWithUuid.contains(config_key::server_uuid)) {
QString uuid = QUuid::createUuid().toString();
uuid.remove(0, 1);
uuid.chop(1);
serverWithUuid.insert(config_key::server_uuid, uuid);
}
QJsonArray servers = serversArray();
servers.append(server);
servers.append(serverWithUuid);
setServersArray(servers);
}
@@ -73,8 +86,15 @@ void Settings::removeServer(int index)
if (index >= servers.size())
return;
const QString removedUuid = servers.at(index).toObject().value(config_key::server_uuid).toString();
servers.removeAt(index);
setServersArray(servers);
if (!removedUuid.isEmpty() && removedUuid == localProxyOwnerUuid()) {
m_settings.setValue("Conf/localProxyHttpEnabled", false);
m_settings.setValue("Conf/localProxyOwnerUuid", "");
emit localProxySettingsChanged();
}
emit serverRemoved(index);
}
@@ -478,6 +498,31 @@ void Settings::setInstallationUuid(const QString &uuid)
m_settings.setValue("Conf/installationUuid", uuid);
}
void Settings::migrateServerUuids()
{
QJsonArray servers = serversArray();
bool hasChanges = false;
for (int i = 0; i < servers.size(); ++i) {
QJsonObject server = servers.at(i).toObject();
if (!server.contains(config_key::server_uuid)) {
QString uuid = QUuid::createUuid().toString();
qDebug() << "Migrating server uuid: " << uuid;
// Remove {} from uuid (as in getInstallationUuid)
uuid.remove(0, 1);
uuid.chop(1);
server.insert(config_key::server_uuid, uuid);
servers.replace(i, server);
qDebug() << "Server uuid migrated: " << server;
hasChanges = true;
}
}
if (hasChanges) {
setServersArray(servers);
}
}
ServerCredentials Settings::defaultServerCredentials() const
{
return serverCredentials(defaultServerIndex());
@@ -565,3 +610,59 @@ void Settings::setReadNewsIds(const QStringList &ids)
{
m_settings.setValue("News/readIds", ids);
}
QString Settings::localProxyOwnerUuid() const
{
return m_settings.value("Conf/localProxyOwnerUuid", "").toString();
}
void Settings::setLocalProxyOwnerUuid(const QString &uuid)
{
m_settings.setValue("Conf/localProxyOwnerUuid", uuid);
emit localProxySettingsChanged();
}
quint16 Settings::localProxyPort() const
{
return m_settings.value("Conf/localProxyPort", 10808).toUInt();
}
void Settings::setLocalProxyPort(quint16 port)
{
m_settings.setValue("Conf/localProxyPort", port);
emit localProxySettingsChanged();
}
bool Settings::isLocalProxyPortUserDefined() const
{
return m_settings.value("Conf/localProxyPortUserDefined", false).toBool();
}
void Settings::setLocalProxyPortUserDefined(bool userDefined)
{
m_settings.setValue("Conf/localProxyPortUserDefined", userDefined);
}
bool Settings::isLocalProxyHttpEnabled() const
{
return m_settings.value("Conf/localProxyHttpEnabled", false).toBool();
}
void Settings::setLocalProxyHttpEnabled(bool enabled)
{
m_settings.setValue("Conf/localProxyHttpEnabled", enabled);
emit localProxySettingsChanged();
}
int Settings::localProxyRestartToken() const
{
return m_settings.value("Conf/localProxyRestartToken", 0).toInt();
}
void Settings::bumpLocalProxyRestartToken()
{
const int current = localProxyRestartToken();
const int next = (current == std::numeric_limits<int>::max()) ? 0 : (current + 1);
m_settings.setValue("Conf/localProxyRestartToken", next);
emit localProxySettingsChanged();
}

View File

@@ -247,15 +247,31 @@ public:
QStringList readNewsIds() const;
void setReadNewsIds(const QStringList &ids);
// Local proxy settings
QString localProxyOwnerUuid() const;
void setLocalProxyOwnerUuid(const QString &uuid);
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 saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
void serverRemoved(int serverIndex);
void settingsCleared();
void localProxySettingsChanged();
void localProxyStartFailed(const QString &message);
private:
void setInstallationUuid(const QString &uuid);
void migrateServerUuids();
mutable SecureQSettings m_settings;
QString m_gatewayEndpoint;

View File

@@ -933,6 +933,7 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email)
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig)
{
const QString serverUuid = m_serversModel->getServerUuid(serverIndex);
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
@@ -983,6 +984,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(configKey::apiConfig, newApiConfig);
newServerConfig.insert(configKey::authData, gatewayRequestData.authData);
newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc));
newServerConfig.insert(config_key::server_uuid, serverConfig.value(config_key::server_uuid));
if (serverConfig.value(config_key::nameOverriddenByUser).toBool()) {
newServerConfig.insert(config_key::name, serverConfig.value(config_key::name));
@@ -994,6 +996,14 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
emit subscriptionRefreshNeeded();
}
if (!serverUuid.isEmpty()
&& m_settings
&& m_settings->isLocalProxyHttpEnabled()
&& m_settings->localProxyOwnerUuid() == serverUuid) {
m_settings->bumpLocalProxyRestartToken();
}
if (reloadServiceConfig) {
emit reloadServerFromApiFinished(tr("API config reloaded"));
} else if (newCountryName.isEmpty()) {
@@ -1030,6 +1040,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
m_settings->isStrictKillSwitchEnabled());
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
const QString serverUuid = m_serversModel->getServerUuid(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
QString serviceProtocol = serverConfig.value(configKey::protocol).toString();
@@ -1054,6 +1065,14 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
}
m_serversModel->editServer(serverConfig, serverIndex);
if (!serverUuid.isEmpty()
&& m_settings
&& m_settings->isLocalProxyHttpEnabled()
&& m_settings->localProxyOwnerUuid() == serverUuid) {
m_settings->bumpLocalProxyRestartToken();
}
emit updateServerFromApiFinished();
return true;
} else {

View File

@@ -34,6 +34,11 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection()
{
if (m_settings->isLocalProxyHttpEnabled()) {
m_settings->setLocalProxyHttpEnabled(false);
emit localProxyStoppedBecauseVpnTurnedOn(tr("Local proxy stopped because VPN was turned on"));
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{

View File

@@ -44,6 +44,7 @@ signals:
void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void disconnectFromVpn();
void connectionStateChanged();
void localProxyStoppedBecauseVpnTurnedOn(const QString &message);
void connectionErrorOccurred(ErrorCode errorCode);
void reconnectWithUpdatedContainer(const QString &message);

View File

@@ -42,6 +42,8 @@ namespace PageLoader
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions,
PageSettingsConnectionType,
PageSettingsLocalProxy,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,

View File

@@ -4,6 +4,9 @@
#include <QOperatingSystemVersion>
#include "logger.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "core/local-proxy/portavailabilityhelper.h"
#endif
#include "systemController.h"
#include "ui/qautostart.h"
#include "amnezia_application.h"
@@ -16,6 +19,12 @@
#include <AmneziaVPN-Swift.h>
#endif
namespace {
constexpr int kDefaultProxyPort = 10808;
constexpr int kLocalProxyPortMin = 1024;
constexpr int kLocalProxyPortMax = 65535;
}
SettingsController::SettingsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ContainersModel> &containersModel,
const QSharedPointer<LanguageModel> &languageModel,
@@ -51,6 +60,9 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
connect(m_settings.get(), &Settings::localProxySettingsChanged, this, &SettingsController::localProxySettingsUpdated);
connect(m_settings.get(), &Settings::localProxyStartFailed, this, &SettingsController::localProxyStartFailed);
}
QString getPlatformName()
@@ -533,3 +545,115 @@ void SettingsController::disableHomeAdLabel()
m_settings->disableHomeAdLabel();
emit isHomeAdLabelVisibleChanged(false);
}
bool SettingsController::isLocalProxySupported() const
{
#ifdef AMNEZIA_DESKTOP
return true;
#else
return false;
#endif
}
bool SettingsController::isLocalProxyHttpEnabled() const
{
return m_settings->isLocalProxyHttpEnabled();
}
int SettingsController::localProxyPort() const
{
return static_cast<int>(m_settings->localProxyPort());
}
QString SettingsController::localProxyOwnerUuid() const
{
return m_settings->localProxyOwnerUuid();
}
bool SettingsController::setLocalProxyPort(int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
return false;
}
if (m_settings->localProxyPort() == static_cast<quint16>(port)) {
m_settings->setLocalProxyPortUserDefined(true);
return true;
}
m_settings->setLocalProxyPort(static_cast<quint16>(port));
m_settings->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
}
bool SettingsController::isLocalProxyPortUserDefined() const
{
return m_settings->isLocalProxyPortUserDefined();
}
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 &ownerUuid, int port)
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
Q_UNUSED(ownerUuid);
Q_UNUSED(port);
return false;
#else
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerUuid.isEmpty()) {
return false;
}
if (m_settings->isLocalProxyHttpEnabled() && m_settings->localProxyOwnerUuid() != ownerUuid) {
return false;
}
int selectedPort = port;
const bool isUserDefinedPort = m_settings->isLocalProxyPortUserDefined();
if (isUserDefinedPort) {
if (!PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
} else if (selectedPort != kDefaultProxyPort && !PortAvailabilityHelper::isPortAvailable(selectedPort)) {
return false;
}
if (m_settings->localProxyPort() != static_cast<quint16>(selectedPort)) {
m_settings->setLocalProxyPort(static_cast<quint16>(selectedPort));
}
m_settings->setLocalProxyPortUserDefined(isUserDefinedPort);
m_settings->setLocalProxyOwnerUuid(ownerUuid);
m_settings->setLocalProxyHttpEnabled(true);
return true;
#endif
}
void SettingsController::disableLocalProxy()
{
if (m_settings->isLocalProxyHttpEnabled()) {
m_settings->setLocalProxyHttpEnabled(false);
}
}

View File

@@ -36,6 +36,10 @@ public:
Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged)
Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin NOTIFY safeAreaBottomMarginChanged)
Q_PROPERTY(int imeHeight READ getImeHeight NOTIFY imeHeightChanged)
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 localProxyOwnerUuid READ localProxyOwnerUuid NOTIFY localProxySettingsUpdated)
public slots:
void toggleAmneziaDns(bool enable);
@@ -112,6 +116,17 @@ public slots:
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
bool isLocalProxySupported() const;
bool isLocalProxyHttpEnabled() const;
int localProxyPort() const;
QString localProxyOwnerUuid() const;
bool setLocalProxyPort(int port);
bool isLocalProxyPortBusy(int port) const;
bool isLocalProxyPortUserDefined() const;
int findFirstAvailableLocalProxyPort(int startPort) const;
bool enableLocalProxy(const QString &ownerUuid, int port);
void disableLocalProxy();
signals:
void primaryDnsChanged();
void secondaryDnsChanged();
@@ -146,6 +161,8 @@ signals:
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
void localProxySettingsUpdated();
void localProxyStartFailed(const QString &message);
private:
QSharedPointer<ServersModel> m_serversModel;

View File

@@ -213,6 +213,9 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
}
return apiUtils::isSubscriptionExpiringSoon(endDate);
}
case ServerUuidRole: {
return server.value(config_key::server_uuid).toString();
}
}
return QVariant();
@@ -481,6 +484,8 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[ServerUuidRole] = "serverUuid";
return roles;
}
@@ -1010,3 +1015,18 @@ QString ServersModel::adDescription()
{
return data(m_defaultServerIndex, AdDescriptionRole).toString();
}
QString ServersModel::getServerUuid(int index) const
{
if (index < 0 || index >= m_servers.size())
return QString();
return m_servers.at(index).toObject().value(config_key::server_uuid).toString();
}
QString ServersModel::getProcessedServerUuid() const
{
qDebug() << "getProcessedServerIndex" << m_processedServerIndex;
qDebug() << "getServerUuid" << getServerUuid(m_processedServerIndex);
return getServerUuid(m_processedServerIndex);
}

View File

@@ -56,7 +56,9 @@ public:
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
HasAmneziaDns
HasAmneziaDns,
ServerUuidRole
};
ServersModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
@@ -86,6 +88,7 @@ public:
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
Q_PROPERTY(QString processedServerUuid READ getProcessedServerUuid NOTIFY processedServerChanged)
Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged)
@@ -160,6 +163,10 @@ public slots:
bool isAdVisible();
QString adHeader();
QString adDescription();
QString getServerUuid(int index) const;
QString getProcessedServerUuid() const;
protected:
QHash<int, QByteArray> roleNames() const override;

View File

@@ -12,6 +12,7 @@ Item {
property int headerTextMaximumLineCount: 2
property int headerTextElide: Qt.ElideRight
property string descriptionText
property string descriptionColor: AmneziaStyle.color.mutedGray
property alias headerRow: headerRow
implicitWidth: content.implicitWidth
@@ -38,7 +39,7 @@ Item {
Layout.topMargin: 16
Layout.fillWidth: true
text: root.descriptionText
color: AmneziaStyle.color.mutedGray
color: root.descriptionColor
visible: root.descriptionText !== ""
}
}

View File

@@ -1,228 +1,232 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "TextTypes"
Item {
id: root
property string headerText
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
property string headerTextColor: AmneziaStyle.color.mutedGray
property alias errorText: errorField.text
property bool checkEmptyText: false
property bool rightButtonClickedOnEnter: false
property string buttonText
property string buttonImageSource
property var clickedFunc
property alias textField: textField
property string textFieldTextColor: AmneziaStyle.color.paleGray
property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray
property bool textFieldEditable: true
property string borderColor: AmneziaStyle.color.slateGray
property string borderFocusedColor: AmneziaStyle.color.paleGray
property string backgroundColor: AmneziaStyle.color.onyxBlack
property string backgroundDisabledColor: AmneziaStyle.color.transparent
property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
ColumnLayout {
id: content
anchors.fill: parent
Rectangle {
id: backgroud
Layout.fillWidth: true
Layout.preferredHeight: input.implicitHeight
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
radius: 16
border.color: getBackgroundBorderColor(root.borderColor)
border.width: 1
Behavior on border.color {
PropertyAnimation { duration: 200 }
}
RowLayout {
id: input
anchors.fill: backgroud
ColumnLayout {
Layout.margins: 16
LabelTextType {
text: root.headerText
color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor
visible: text !== ""
Layout.fillWidth: true
}
TextField {
id: textField
property bool isFocusable: true
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
enabled: root.textFieldEditable
color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
placeholderTextColor: AmneziaStyle.color.charcoalGray
selectionColor: AmneziaStyle.color.richBrown
selectedTextColor: AmneziaStyle.color.paleGray
font.pixelSize: 16
font.weight: 400
font.family: "PT Root UI VF"
height: 24
Layout.fillWidth: true
topPadding: 0
rightPadding: 0
leftPadding: 0
bottomPadding: 0
background: Rectangle {
anchors.fill: parent
color: root.backgroundDisabledColor
}
onTextChanged: {
root.errorText = ""
}
onActiveFocusChanged: {
if (root.checkEmptyText && text === "") {
root.errorText = qsTr("The field can't be empty")
}
}
ContextMenu.menu: ContextMenuType {
textObj: textField
}
onFocusChanged: {
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
}
}
}
}
SmallTextType {
id: errorField
text: root.errorText
visible: root.errorText !== ""
color: AmneziaStyle.color.vibrantRed
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: root
cursorShape: Qt.IBeamCursor
hoverEnabled: true
onPressed: function(mouse) {
textField.forceActiveFocus()
mouse.accepted = false
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
onEntered: {
backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor)
}
onExited: {
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
}
BasicButtonType {
visible: (root.buttonText !== "") || (root.buttonImageSource !== "")
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
anchors.top: content.top
anchors.bottom: content.bottom
anchors.right: content.right
height: content.implicitHeight
width: content.implicitHeight
squareLeftSide: true
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {
root.clickedFunc()
}
}
}
function getBackgroundBorderColor(noneFocusedColor) {
return textField.focus ? root.borderFocusedColor : noneFocusedColor
}
Keys.onEnterPressed: {
if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") {
clickedFunc()
}
// if (KeyNavigation.tab) {
// KeyNavigation.tab.forceActiveFocus();
// }
}
Keys.onReturnPressed: {
if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") {
clickedFunc()
}
// if (KeyNavigation.tab) {
// KeyNavigation.tab.forceActiveFocus();
// }
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "TextTypes"
Item {
id: root
property string headerText
property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray
property string headerTextColor: AmneziaStyle.color.mutedGray
property alias errorText: errorField.text
property bool clearErrorOnTextChanged: true
property bool checkEmptyText: false
property bool rightButtonClickedOnEnter: false
property string buttonText
property string buttonImageSource
property var clickedFunc
property alias textField: textField
property string textFieldTextColor: AmneziaStyle.color.paleGray
property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray
property bool textFieldEditable: true
property string borderColor: AmneziaStyle.color.slateGray
property string borderFocusedColor: AmneziaStyle.color.paleGray
property string backgroundColor: AmneziaStyle.color.onyxBlack
property string backgroundDisabledColor: AmneziaStyle.color.transparent
property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
ColumnLayout {
id: content
anchors.fill: parent
Rectangle {
id: backgroud
Layout.fillWidth: true
Layout.preferredHeight: input.implicitHeight
color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor
radius: 16
border.color: getBackgroundBorderColor(root.borderColor)
border.width: 1
Behavior on border.color {
PropertyAnimation { duration: 200 }
}
RowLayout {
id: input
anchors.fill: backgroud
ColumnLayout {
Layout.margins: 16
LabelTextType {
text: root.headerText
color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor
visible: text !== ""
Layout.fillWidth: true
}
TextField {
id: textField
property bool isFocusable: true
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
enabled: root.textFieldEditable
color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
placeholderTextColor: AmneziaStyle.color.charcoalGray
selectionColor: AmneziaStyle.color.richBrown
selectedTextColor: AmneziaStyle.color.paleGray
font.pixelSize: 16
font.weight: 400
font.family: "PT Root UI VF"
height: 24
Layout.fillWidth: true
topPadding: 0
rightPadding: 0
leftPadding: 0
bottomPadding: 0
background: Rectangle {
anchors.fill: parent
color: root.backgroundDisabledColor
}
onTextChanged: {
if (root.clearErrorOnTextChanged) {
root.errorText = ""
}
}
onActiveFocusChanged: {
if (root.checkEmptyText && text === "") {
root.errorText = qsTr("The field can't be empty")
}
}
ContextMenu.menu: ContextMenuType {
textObj: textField
}
onFocusChanged: {
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
}
}
}
}
SmallTextType {
id: errorField
text: root.errorText
visible: root.errorText !== ""
color: AmneziaStyle.color.vibrantRed
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: root
cursorShape: Qt.IBeamCursor
hoverEnabled: true
onPressed: function(mouse) {
textField.forceActiveFocus()
mouse.accepted = false
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
onEntered: {
backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor)
}
onExited: {
backgroud.border.color = getBackgroundBorderColor(root.borderColor)
}
}
BasicButtonType {
visible: (root.buttonText !== "") || (root.buttonImageSource !== "")
parent: backgroud
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
height: parent.height
width: Math.max(height, implicitWidth)
squareLeftSide: true
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {
root.clickedFunc()
}
}
}
function getBackgroundBorderColor(noneFocusedColor) {
return textField.focus ? root.borderFocusedColor : noneFocusedColor
}
Keys.onEnterPressed: {
if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") {
clickedFunc()
}
// if (KeyNavigation.tab) {
// KeyNavigation.tab.forceActiveFocus();
// }
}
Keys.onReturnPressed: {
if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") {
clickedFunc()
}
// if (KeyNavigation.tab) {
// KeyNavigation.tab.forceActiveFocus();
// }
}
}

View File

@@ -311,11 +311,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")

View 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 + SettingsController.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 && ServersModel.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
}
}
}
}

View 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 pendingEnableServerUuid: ""
property int pendingEnableRequestedPort: -1
property int pendingEnableAutoSelectedPort: -1
property int pendingEnablePortToUse: -1
function clearPendingEnableAfterVpnDisconnect() {
root.pendingEnableAfterVpnDisconnect = false
root.pendingEnableServerUuid = ""
root.pendingEnableRequestedPort = -1
root.pendingEnableAutoSelectedPort = -1
root.pendingEnablePortToUse = -1
}
function enableLocalProxyNow(serverUuid, requestedPort, autoSelectedPort, portToEnable, vpnWasActive) {
if (!SettingsController.enableLocalProxy(serverUuid, 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 (!ServersModel.processedServerIsPremium) {
PageController.showNotificationMessage(qsTr("Local proxy is available only for Amnezia Premium"))
return
}
const wasVpnActive = ConnectionController.isConnected || ConnectionController.isConnectionInProgress
let serverUuid = ServersModel.processedServerUuid
if (!serverUuid && ServersModel.defaultIndex !== undefined) {
serverUuid = ServersModel.getServerUuid(ServersModel.defaultIndex)
}
if (!serverUuid) {
PageController.showNotificationMessage(qsTr("Unable to determine the current server"))
return
}
if (SettingsController.isLocalProxyHttpEnabled
&& SettingsController.localProxyOwnerUuid
&& SettingsController.localProxyOwnerUuid !== serverUuid) {
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.pendingEnableServerUuid = serverUuid
root.pendingEnableRequestedPort = requestedPort
root.pendingEnableAutoSelectedPort = autoSelectedPort
root.pendingEnablePortToUse = portToEnable
ConnectionController.closeConnection()
return
}
root.enableLocalProxyNow(serverUuid, 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 + SettingsController.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: ServersModel.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 serverUuid = root.pendingEnableServerUuid
const requestedPort = root.pendingEnableRequestedPort
const autoSelectedPort = root.pendingEnableAutoSelectedPort
const portToEnable = root.pendingEnablePortToUse
root.clearPendingEnableAfterVpnDisconnect()
root.enableLocalProxyNow(serverUuid, 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: ServersModel
function onProcessedServerChanged() {
var portField = root.getPortField()
if (portField !== null && !portField.textField.activeFocus) {
portField.syncPortValue()
}
}
}
}