fix: Fix local proxy UI

This commit is contained in:
aiamnezia
2026-02-13 15:46:27 +04:00
parent 4c2010244b
commit 6518d4866e
14 changed files with 263 additions and 82 deletions
+6 -1
View File
@@ -61,7 +61,12 @@ void CoreController::initLocalProxy()
return;
}
m_proxyServer->syncSettings();
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;
};
+19 -31
View File
@@ -5,11 +5,11 @@
#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 <QHostAddress>
#include <QDir>
#include <QFile>
#include <QJsonArray>
@@ -18,7 +18,6 @@
#include <QSaveFile>
#include <QSysInfo>
#include <QStandardPaths>
#include <QTcpServer>
#include <QUuid>
ConfigManager::ConfigManager(const std::shared_ptr<Settings> &settings)
@@ -84,18 +83,6 @@ QString ConfigManager::serializeConfig(const QJsonObject &config) const
return QString::fromUtf8(QJsonDocument(config).toJson(QJsonDocument::Compact));
}
bool ConfigManager::isPortAvailable(int port) const
{
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<ConfigManager::ConfigData> ConfigManager::buildConfig(QString &errorDescription) const
{
errorDescription.clear();
@@ -223,29 +210,30 @@ std::optional<ConfigManager::ConfigData> ConfigManager::buildConfigWithFetch(QSt
data.parsedConfig = doc.object();
int selectedPort = resolveProxyPort(m_settings);
const int startPort = selectedPort;
const bool isUserDefinedPort = m_settings->isLocalProxyPortUserDefined();
bool found = false;
for (int port = selectedPort; port <= kProxyPortMax; ++port) {
if (isPortAvailable(port)) {
selectedPort = port;
found = true;
break;
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 (!found) {
errorDescription = QStringLiteral("No available local proxy port in range %1-%2")
.arg(startPort)
.arg(kProxyPortMax);
ProxyLogger::getInstance().error(errorDescription);
return std::nullopt;
}
if (applyProxyPortToConfig(data.parsedConfig, selectedPort)) {
data.serializedConfig = serializeConfig(data.parsedConfig);
if (m_settings && m_settings->localProxyPort() != static_cast<quint16>(selectedPort)) {
m_settings->setLocalProxyPort(static_cast<quint16>(selectedPort));
}
} else {
ProxyLogger::getInstance().warning(QStringLiteral("Failed to override local proxy inbound port; using original config"));
data.serializedConfig = *serializedConfig;
-1
View File
@@ -32,7 +32,6 @@ private:
QString tempDirectory() const;
bool applyProxyPortToConfig(QJsonObject &config, int port) const;
QString serializeConfig(const QJsonObject &config) const;
bool isPortAvailable(int port) const;
std::shared_ptr<Settings> m_settings;
};
@@ -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;
}
@@ -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);
};
+14 -7
View File
@@ -65,11 +65,11 @@ void ProxyServer::stopXrayProcess()
m_service->stopXray();
}
void ProxyServer::syncSettings()
bool ProxyServer::syncSettings()
{
if (!m_isRunning) {
qDebug() << "Local proxy: syncSettings called but server is not running";
return;
return false;
}
const quint16 newProxyPort = m_settings ? m_settings->localProxyPort() : 0;
@@ -77,14 +77,21 @@ void ProxyServer::syncSettings()
if (!xrayRunning) {
qInfo() << "Local proxy: starting Xray on port" << newProxyPort;
m_currentProxyPort = newProxyPort;
startXrayProcess();
return;
const bool started = startXrayProcess();
if (started) {
m_currentProxyPort = newProxyPort;
}
return started;
}
if (m_currentProxyPort != newProxyPort) {
qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort;
m_currentProxyPort = newProxyPort;
m_service->restartXray();
const bool restarted = m_service->restartXray();
if (restarted) {
m_currentProxyPort = newProxyPort;
}
return restarted;
}
return true;
}
+1 -1
View File
@@ -20,7 +20,7 @@ public:
bool start(quint16 port = 49490);
void stop();
void syncSettings();
bool syncSettings();
private:
bool startXrayProcess();
+10
View File
@@ -647,6 +647,16 @@ void Settings::setLocalProxyPort(quint16 port)
emit localProxySettingsChanged();
}
bool Settings::isLocalProxyPortUserDefined() const
{
return value("Conf/localProxyPortUserDefined", false).toBool();
}
void Settings::setLocalProxyPortUserDefined(bool userDefined)
{
setValue("Conf/localProxyPortUserDefined", userDefined);
}
bool Settings::isLocalProxyHttpEnabled() const
{
return value("Conf/localProxyHttpEnabled", false).toBool();
+2
View File
@@ -244,6 +244,8 @@ public:
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);
+36 -1
View File
@@ -4,6 +4,7 @@
#include <QOperatingSystemVersion>
#include "logger.h"
#include "core/local-proxy/portavailabilityhelper.h"
#include "systemController.h"
#include "ui/qautostart.h"
#include "amnezia_application.h"
@@ -17,6 +18,7 @@
#endif
namespace {
constexpr int kDefaultProxyPort = 10808;
constexpr int kLocalProxyPortMin = 1024;
constexpr int kLocalProxyPortMax = 65535;
}
@@ -563,13 +565,31 @@ bool SettingsController::setLocalProxyPort(int port)
}
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
{
return !PortAvailabilityHelper::isPortAvailable(port);
}
bool SettingsController::isLocalProxyPortUserDefined() const
{
return m_settings->isLocalProxyPortUserDefined();
}
int SettingsController::findFirstAvailableLocalProxyPort(int startPort) const
{
const auto port = PortAvailabilityHelper::findFirstAvailablePort(startPort, kLocalProxyPortMax);
return port ? *port : -1;
}
bool SettingsController::enableLocalProxy(const QString &ownerUuid, int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerUuid.isEmpty()) {
@@ -580,8 +600,23 @@ bool SettingsController::enableLocalProxy(const QString &ownerUuid, int port)
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);
setLocalProxyPort(port);
m_settings->setLocalProxyHttpEnabled(true);
return true;
@@ -118,6 +118,9 @@ public slots:
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();
+2 -1
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 !== ""
}
}
@@ -14,6 +14,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
@@ -128,7 +129,9 @@ Item {
}
onTextChanged: {
root.errorText = ""
if (root.clearErrorOnTextChanged) {
root.errorText = ""
}
}
onActiveFocusChanged: {
+112 -38
View File
@@ -17,13 +17,25 @@ PageType {
readonly property int localProxyPortMax: 65535
readonly property int defaultLocalProxyPort: 10808
property bool portValidationEnabled: false
property string portValidationError: ""
property bool suppressToggleHandler: false
property int pendingStartRequestedPort: -1
property int pendingStartAutoSelectedPort: -1
Component.onCompleted: root.syncSwitchState()
function getPortField() {
var item = listView.itemAtIndex(0)
return item !== null ? item.children[0] : null
}
function getHeaderBlock() {
var headerItem = listView.headerItem
return headerItem && headerItem.children.length > 0 ? headerItem.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")
@@ -34,18 +46,11 @@ PageType {
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax)
}
return ""
}
function updatePortValidation(force) {
if (force) {
root.portValidationEnabled = true
if (SettingsController.isLocalProxyPortBusy(value)) {
return qsTr("Port %1 is already in use on this device. Choose another one")
.arg(value)
}
root.portValidationError = root.portValidationEnabled ? root.computePortErrorText() : ""
}
function isPortValid() {
return root.computePortErrorText() === ""
return ""
}
function syncSwitchState() {
@@ -53,11 +58,11 @@ PageType {
}
function setSwitcherChecked(value) {
if (localProxyHeader.switcher.checked === value) {
var header = getHeaderBlock()
if (!header || header.switcher.checked === value) {
return
}
root.suppressToggleHandler = true
localProxyHeader.switcher.checked = value
header.switcher.checked = value
}
function handleLocalProxyToggle(checked) {
@@ -77,7 +82,7 @@ PageType {
return
}
const requestedPort = portField.portValue()
const requestedPort = SettingsController.localProxyPort
if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) {
root.setSwitcherChecked(false)
PageController.showNotificationMessage(qsTr("Port must be between %1 and %2")
@@ -86,6 +91,25 @@ PageType {
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))
root.setSwitcherChecked(false)
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))
root.setSwitcherChecked(false)
return
}
}
if (!SettingsController.enableLocalProxy(serverUuid, requestedPort)) {
root.setSwitcherChecked(false)
PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2).")
@@ -94,8 +118,14 @@ PageType {
root.syncSwitchState()
return
}
root.pendingStartRequestedPort = requestedPort
root.pendingStartAutoSelectedPort = autoSelectedPort
startSuccessToastTimer.restart()
root.syncSwitchState()
} else {
startSuccessToastTimer.stop()
root.pendingStartRequestedPort = -1
root.pendingStartAutoSelectedPort = -1
SettingsController.disableLocalProxy()
root.syncSwitchState()
}
@@ -136,9 +166,10 @@ PageType {
headerText: qsTr("Local Proxy")
descriptionText: qsTr("Use a proxy to route selected apps (for example, the CensorTracker extension) through Amnezia Premium.")
showSwitcher: true
Component.onCompleted: root.syncSwitchState()
switcherFunction: function(checked) {
if (root.suppressToggleHandler) {
root.suppressToggleHandler = false
// Ignore UI sync toggles; react only to real state change intent.
if (checked === SettingsController.isLocalProxyHttpEnabled) {
return
}
root.handleLocalProxyToggle(checked)
@@ -147,16 +178,18 @@ PageType {
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 8
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
@@ -167,7 +200,11 @@ PageType {
text: qsTr("Learn more")
clickedFunc: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl())
const isRussian = LanguageModel.currentLanguageName === "Русский"
const learnMoreUrl = isRussian
? "http://docs.amnezia.org/ru/documentation/instructions/local-proxy"
: "http://docs.amnezia.org/documentation/instructions/local-proxy"
Qt.openUrlExternally(learnMoreUrl)
}
}
}
@@ -188,26 +225,29 @@ PageType {
headerText: qsTr("Address and port")
buttonText: qsTr("Copy")
errorText: root.portValidationError
clearErrorOnTextChanged: false
enabled: true
rightButtonClickedOnEnter: 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: IntValidator {
bottom: root.localProxyPortMin
top: root.localProxyPortMax
}
textField.leftPadding: portPrefix.implicitWidth + 8
textField.leftPadding: portPrefix.implicitWidth
textField.placeholderText: root.defaultLocalProxyPort.toString()
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
function syncPortValue() {
const port = SettingsController.localProxyPort
textField.text = (port >= root.localProxyPortMin && port <= root.localProxyPortMax) ? port.toString() : ""
const isValidPort = port >= root.localProxyPortMin && port <= root.localProxyPortMax
textField.text = (isValidPort && port !== root.defaultLocalProxyPort) ? port.toString() : ""
}
function portValue() {
@@ -229,24 +269,26 @@ PageType {
Component.onCompleted: syncPortValue()
textField.onTextChanged: root.updatePortValidation(false)
textField.onActiveFocusChanged: {
if (!textField.activeFocus) {
root.updatePortValidation(true)
textField.onTextChanged: {
if (textField.activeFocus) {
root.portValidationError = ""
}
}
}
LabelTextType {
Text {
id: portPrefix
parent: portField
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: portField.textField.left
anchors.verticalCenter: portField.textField.verticalCenter
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
BasicButtonType {
@@ -255,18 +297,22 @@ PageType {
Layout.rightMargin: 16
text: qsTr("Save")
enabled: root.isPortValid()
enabled: true
clickedFunc: function() {
root.updatePortValidation(true)
if (!root.isPortValid()) {
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()
}
@@ -274,17 +320,45 @@ PageType {
}
}
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.pendingStartRequestedPort > 0) {
PageController.showNotificationMessage(qsTr("Local proxy is running: 127.0.0.1:%1")
.arg(root.pendingStartRequestedPort))
}
root.pendingStartRequestedPort = -1
root.pendingStartAutoSelectedPort = -1
}
}
Connections {
target: SettingsController
function onLocalProxySettingsUpdated() {
root.syncSwitchState()
if (!portField.textField.activeFocus) {
var portField = root.getPortField()
if (portField !== null && !portField.textField.activeFocus) {
portField.syncPortValue()
}
}
function onLocalProxyStartFailed(message) {
startSuccessToastTimer.stop()
root.pendingStartRequestedPort = -1
root.pendingStartAutoSelectedPort = -1
PageController.showNotificationMessage(message)
root.syncSwitchState()
}
@@ -295,10 +369,10 @@ PageType {
function onProcessedServerChanged() {
root.syncSwitchState()
if (!portField.textField.activeFocus) {
var portField = root.getPortField()
if (portField !== null && !portField.textField.activeFocus) {
portField.syncPortValue()
}
}
}
}