feat: add mtproxy(#2370)

* Feat: Add MtProxy (Telegram)

* add path files

* refactor: move logic from ui to core

---------

Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
yp
2026-05-18 14:52:58 +03:00
committed by GitHub
parent 8c33779fc3
commit 277b295fd8
52 changed files with 4062 additions and 30 deletions
@@ -0,0 +1,46 @@
#include "networkReachabilityController.h"
#include <QNetworkInformation>
namespace {
bool reachabilityAllowsRemoteOperations(QNetworkInformation::Reachability r) {
using R = QNetworkInformation::Reachability;
// Unknown: no backend or not yet determined — do not block UI.
return r == R::Online || r == R::Unknown;
}
} // namespace
NetworkReachabilityController::NetworkReachabilityController(QObject *parent) : QObject(parent) {
attachToNetworkInformation();
}
bool NetworkReachabilityController::hasInternetAccess() const {
return m_hasInternetAccess;
}
void NetworkReachabilityController::attachToNetworkInformation() {
if (!QNetworkInformation::loadDefaultBackend()) {
return;
}
QNetworkInformation *ni = QNetworkInformation::instance();
if (!ni) {
return;
}
const bool initial = reachabilityAllowsRemoteOperations(ni->reachability());
const bool previous = m_hasInternetAccess;
m_hasInternetAccess = initial;
if (previous != m_hasInternetAccess) {
emit hasInternetAccessChanged();
}
connect(ni, &QNetworkInformation::reachabilityChanged, this,
[this](QNetworkInformation::Reachability r) {
const bool ok = reachabilityAllowsRemoteOperations(r);
if (ok == m_hasInternetAccess) {
return;
}
m_hasInternetAccess = ok;
emit hasInternetAccessChanged();
});
}
@@ -0,0 +1,30 @@
#ifndef NETWORKREACHABILITYCONTROLLER_H
#define NETWORKREACHABILITYCONTROLLER_H
#include <QObject>
// Exposes QNetworkInformation to QML for UI that must not run remote operations offline.
// Note: mozilla/networkwatcher.h has NetworkWatcher::getReachability() using the same API,
// but networkwatcher.cpp is not linked into the desktop client (only the service process).
class NetworkReachabilityController final : public QObject {
Q_OBJECT
Q_PROPERTY(bool hasInternetAccess READ hasInternetAccess NOTIFY hasInternetAccessChanged)
public:
explicit NetworkReachabilityController(QObject *parent = nullptr);
bool hasInternetAccess() const;
signals:
void hasInternetAccessChanged();
private:
void attachToNetworkInformation();
bool m_hasInternetAccess = true;
};
#endif // NETWORKREACHABILITYCONTROLLER_H
@@ -50,6 +50,7 @@ namespace PageLoader
PageServiceTorWebsiteSettings,
PageServiceDnsSettings,
PageServiceSocksProxySettings,
PageServiceMtProxySettings,
PageSetupWizardStart,
PageSetupWizardCredentials,
@@ -3,6 +3,7 @@
#include <QDebug>
#include "../systemController.h"
#include "core/utils/qrCodeUtils.h"
ExportUiController::ExportUiController(ExportController* exportController, QObject *parent)
: QObject(parent),
@@ -53,6 +54,14 @@ void ExportUiController::generateXrayConfig(const QString &serverId, const QStri
applyExportResult(result);
}
void ExportUiController::generateQrFromString(const QString &text)
{
clearPreviousConfig();
m_config = text;
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(text.toUtf8());
emit exportConfigChanged();
}
QString ExportUiController::getConfig()
{
return m_config;
@@ -25,6 +25,7 @@ public slots:
void generateWireGuardConfig(const QString &serverId, const QString &clientName);
void generateAwgConfig(const QString &serverId, int containerIndex, const QString &clientName);
void generateXrayConfig(const QString &serverId, const QString &clientName);
void generateQrFromString(const QString &text);
QString getConfig();
QString getNativeConfigString();
@@ -5,11 +5,13 @@
#include <QEventLoop>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QFutureWatcher>
#include <QtConcurrent>
#include "core/utils/api/apiUtils.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
@@ -47,6 +49,7 @@ InstallUiController::InstallUiController(InstallController *installController,
#endif
SftpConfigModel *sftpConfigModel,
Socks5ProxyConfigModel *socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
QObject *parent)
: QObject(parent),
m_installController(installController),
@@ -63,7 +66,8 @@ InstallUiController::InstallUiController(InstallController *installController,
m_ikev2ConfigModel(ikev2ConfigModel),
#endif
m_sftpConfigModel(sftpConfigModel),
m_socks5ConfigModel(socks5ConfigModel)
m_socks5ConfigModel(socks5ConfigModel),
m_mtProxyConfigModel(mtConfigModel)
{
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
connect(m_installController, &InstallController::validationErrorOccurred, this, [this](ErrorCode errorCode) {
@@ -199,7 +203,7 @@ void InstallUiController::scanServerForInstalledContainers(const QString &server
emit installationErrorOccurred(errorCode);
}
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex)
void InstallUiController::updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage)
{
DockerContainer container = static_cast<DockerContainer>(containerIndex);
@@ -238,6 +242,10 @@ void InstallUiController::updateContainer(const QString &serverId, int container
containerConfig.protocolConfig = m_socks5ConfigModel->getProtocolConfig();
break;
}
case Proto::MtProxy: {
containerConfig.protocolConfig = m_mtProxyConfigModel->getProtocolConfig();
break;
}
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: {
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
@@ -249,19 +257,113 @@ void InstallUiController::updateContainer(const QString &serverId, int container
}
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
if (container == DockerContainer::MtProxy) {
emit serverIsBusy(true);
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, closePage]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig updatedConfig =
m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
const auto defaultContainer =
m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId())
&& (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
} else {
emit installationErrorOccurred(errorCode);
}
});
ContainerConfig newConfigCopy = containerConfig;
ContainerConfig oldConfigCopy = oldContainerConfig;
InstallController *installController = m_installController;
QFuture<ErrorCode> future =
QtConcurrent::run([installController, serverId, container, oldConfigCopy,
newConfigCopy]() mutable -> ErrorCode {
return installController->updateContainer(serverId, container, oldConfigCopy, newConfigCopy);
});
watcher->setFuture(future);
return;
}
ErrorCode errorCode = m_installController->updateContainer(serverId, container, oldContainerConfig, containerConfig);
if (errorCode == ErrorCode::NoError) {
ContainerConfig updatedConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(updatedConfig);
emit updateContainerFinished(tr("Settings updated successfully"));
const auto defaultContainer = m_serversController->getDefaultContainer(serverId);
if ((serverId == m_serversController->getDefaultServerId()) && (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"), closePage);
}
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
emit serverIsBusy(true);
const ErrorCode errorCode = m_installController->setDockerContainerEnabledState(serverId, container, enabled);
emit serverIsBusy(false);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(currentConfig);
emit setContainerEnabledFinished(enabled);
return;
}
emit installationErrorOccurred(errorCode);
}
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
int status = 3;
const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status);
if (errorCode != ErrorCode::NoError) {
emit containerStatusRefreshed(3);
return;
}
emit containerStatusRefreshed(status);
}
void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
MtProxyContainerDiagnostics diag;
const ErrorCode errorCode = m_installController->queryMtProxyDiagnostics(serverId, container, port, diag);
if (errorCode != ErrorCode::NoError) {
emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString());
return;
}
emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected,
diag.lastConfigRefresh, diag.statsEndpoint);
}
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
{
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
const QString secret = m_installController->fetchDockerContainerSecret(serverId, container);
emit containerSecretFetched(secret);
}
void InstallUiController::rebootServer(const QString &serverId)
{
const QString serverName = m_serversController->notificationDisplayName(serverId);
@@ -473,6 +575,7 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
case Proto::TorWebSite: updateIfPresent(m_torConfigModel, containerConfig.getTorProtocolConfig()); break;
case Proto::Sftp: updateIfPresent(m_sftpConfigModel, containerConfig.getSftpProtocolConfig()); break;
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break;
#ifdef Q_OS_WINDOWS
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
#endif
@@ -28,6 +28,7 @@
#include "ui/models/services/torConfigModel.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "ui/models/services/mtProxyConfigModel.h"
class InstallUiController : public QObject
{
@@ -48,6 +49,7 @@ public:
#endif
SftpConfigModel* sftpConfigModel,
Socks5ProxyConfigModel* socks5ConfigModel,
MtProxyConfigModel* mtConfigModel,
QObject *parent = nullptr);
~InstallUiController();
@@ -58,12 +60,16 @@ public slots:
void scanServerForInstalledContainers(const QString &serverId);
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex);
void updateContainer(const QString &serverId, int containerIndex, int protocolIndex, bool closePage = true);
void removeServer(const QString &serverId);
void rebootServer(const QString &serverId);
void removeAllContainers(const QString &serverId);
void removeContainer(const QString &serverId, int containerIndex);
void setContainerEnabled(const QString &serverId, int containerIndex, bool enabled);
void refreshContainerStatus(const QString &serverId, int containerIndex);
void refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port);
void fetchContainerSecret(const QString &serverId, int containerIndex);
void clearCachedProfile(const QString &serverId, int containerIndex);
@@ -94,7 +100,7 @@ signals:
void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
void installServerFinished(const QString &finishMessage);
void updateContainerFinished(const QString &message);
void updateContainerFinished(const QString &message, bool closePage);
void scanServerFinished(bool isInstalledContainerFound);
@@ -102,6 +108,11 @@ signals:
void removeServerFinished(const QString &finishedMessage);
void removeAllContainersFinished(const QString &finishedMessage);
void removeContainerFinished(const QString &finishedMessage);
void setContainerEnabledFinished(bool enabled);
void containerStatusRefreshed(int status);
void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected,
const QString &lastConfigRefresh, const QString &statsEndpoint);
void containerSecretFetched(const QString &secret);
void installationErrorOccurred(ErrorCode errorCode);
void wrongInstallationUser(const QString &message);
@@ -114,6 +125,8 @@ signals:
void serverIsBusy(const bool isBusy);
void cancelInstallation();
void currentContainerUpdated();
void cachedProfileCleared(const QString &message);
void apiConfigRemoved(const QString &message);
@@ -138,6 +151,7 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
ServerCredentials m_processedServerCredentials;
@@ -510,6 +510,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
servicesName.append("TOR");
} else if (container == DockerContainer::Socks5Proxy) {
servicesName.append("SOCKS5");
} else if (container == DockerContainer::MtProxy) {
servicesName.append("MTProxy");
}
}
}
+2
View File
@@ -74,6 +74,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
case IsSftpRole: return container == DockerContainer::Sftp;
case IsTorWebsiteRole: return container == DockerContainer::TorWebSite;
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
case IsMtProxyRole: return container == DockerContainer::MtProxy;
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
}
@@ -184,5 +185,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsTorWebsiteRole] = "isTorWebsite";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
return roles;
}
+2 -1
View File
@@ -48,7 +48,8 @@ public:
IsDnsRole,
IsSftpRole,
IsTorWebsiteRole,
IsSocks5ProxyRole
IsSocks5ProxyRole,
IsMtProxyRole,
};
Q_INVOKABLE void openContainerSettings(int containerIndex);
+3
View File
@@ -42,6 +42,7 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
roles[IsSftpRole] = "isSftp";
roles[IsIpsecRole] = "isIpsec";
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
roles[IsMtProxyRole] = "isMtProxy";
return roles;
}
@@ -71,6 +72,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
case IsSftpRole: return proto == Proto::Sftp;
case IsIpsecRole: return proto == Proto::Ikev2;
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
case IsMtProxyRole: return proto == Proto::MtProxy;
case RawConfigRole:
return getRawConfig();
case IsClientProtocolExistsRole:
@@ -124,6 +126,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
}
}
+2 -1
View File
@@ -25,7 +25,8 @@ public:
IsXrayRole,
IsSftpRole,
IsIpsecRole,
IsSocks5ProxyRole
IsSocks5ProxyRole,
IsMtProxyRole,
};
explicit ProtocolsModel(QObject *parent = nullptr);
@@ -0,0 +1,714 @@
#include "mtProxyConfigModel.h"
#include "ui/models/utils/mtproxy_public_host_input.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/qrCodeUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/constants/configKeys.h"
#include "qrcodegen.hpp"
#include <QClipboard>
#include <QGuiApplication>
#include <QHostAddress>
#include <QRegExp>
#include <QRegularExpression>
#include <QtGlobal>
#include <qqml.h>
using namespace amnezia;
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
}
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return 1;
}
bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!index.isValid() || index.row() != 0) {
return false;
}
switch (role) {
case Roles::PortRole: {
m_protocolConfig.port = value.toString();
break;
}
case Roles::SecretRole: {
m_protocolConfig.secret = value.toString();
break;
}
case Roles::TagRole: {
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
if (!isValidMtProxyTag(tag)) {
return false;
}
m_protocolConfig.tag = tag;
break;
}
case Roles::IsEnabledRole: {
m_protocolConfig.isEnabled = value.toBool();
break;
}
case Roles::PublicHostRole: {
const QString h = value.toString().trimmed();
if (!isValidPublicHost(h)) {
return false;
}
m_protocolConfig.publicHost = h;
break;
}
case Roles::TransportModeRole: {
m_protocolConfig.transportMode = value.toString();
break;
}
case Roles::TlsDomainRole: {
const QString d = value.toString().trimmed();
if (!isValidFakeTlsDomain(d)) {
return false;
}
m_protocolConfig.tlsDomain = d;
break;
}
case Roles::AdditionalSecretsRole: {
m_protocolConfig.additionalSecrets = value.toStringList();
break;
}
case Roles::WorkersModeRole: {
m_protocolConfig.workersMode = value.toString();
break;
}
case Roles::WorkersRole: {
m_protocolConfig.workers = value.toString();
break;
}
case Roles::NatEnabledRole: {
m_protocolConfig.natEnabled = value.toBool();
break;
}
case Roles::NatInternalIpRole: {
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natInternalIp = ip;
break;
}
case Roles::NatExternalIpRole: {
const QString ip = value.toString().trimmed();
if (!isValidOptionalIpv4(ip)) {
return false;
}
m_protocolConfig.natExternalIp = ip;
break;
}
default: {
return false;
}
}
emit dataChanged(index, index, QList{role});
return true;
}
QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() != 0) {
return QVariant();
}
switch (role) {
case Roles::PortRole: {
return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port;
}
case Roles::SecretRole: {
return m_protocolConfig.secret;
}
case Roles::TagRole: {
return m_protocolConfig.tag;
}
case Roles::TgLinkRole: {
return m_protocolConfig.tgLink;
}
case Roles::TmeLinkRole: {
return m_protocolConfig.tmeLink;
}
case Roles::IsEnabledRole: {
return m_protocolConfig.isEnabled;
}
case Roles::PublicHostRole: {
return m_protocolConfig.publicHost.isEmpty()
? m_fullConfig.value(configKey::hostName).toString()
: m_protocolConfig.publicHost;
}
case Roles::TransportModeRole: {
return m_protocolConfig.transportMode.isEmpty()
? QString(protocols::mtProxy::transportModeStandard)
: m_protocolConfig.transportMode;
}
case Roles::TlsDomainRole: {
return m_protocolConfig.tlsDomain;
}
case Roles::AdditionalSecretsRole: {
return m_protocolConfig.additionalSecrets;
}
case Roles::WorkersModeRole: {
return m_protocolConfig.workersMode.isEmpty()
? QString(protocols::mtProxy::workersModeAuto)
: m_protocolConfig.workersMode;
}
case Roles::WorkersRole: {
return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers)
: m_protocolConfig.workers;
}
case Roles::NatEnabledRole: {
return m_protocolConfig.natEnabled;
}
case Roles::NatInternalIpRole: {
return m_protocolConfig.natInternalIp;
}
case Roles::NatExternalIpRole: {
return m_protocolConfig.natExternalIp;
}
}
return QVariant();
}
void MtProxyConfigModel::updateModel(amnezia::DockerContainer container,
const amnezia::MtProxyProtocolConfig &protocolConfig) {
beginResetModel();
m_container = container;
m_protocolConfig = protocolConfig;
endResetModel();
}
void MtProxyConfigModel::updateModel(const QJsonObject &config) {
beginResetModel();
m_fullConfig = config;
m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject());
if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort;
if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard;
if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto;
if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers;
{
QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag);
if (!isValidMtProxyTag(tagIn)) {
tagIn.clear();
}
m_protocolConfig.tag = tagIn;
}
endResetModel();
}
QJsonObject MtProxyConfigModel::getConfig() {
m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson());
return m_fullConfig;
}
void MtProxyConfigModel::generateSecret() {
// Generate 16 random bytes = 32 hex chars
QString secret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.secret = secret;
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
}
void MtProxyConfigModel::setSecret(const QString &secret) {
if (secret.isEmpty()) {
return;
}
setData(index(0), secret, SecretRole);
}
bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) {
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
return false;
}
setData(index(0), rawSecret, SecretRole);
return true;
}
void MtProxyConfigModel::setPort(const QString &port) {
setData(index(0), port, PortRole);
}
void MtProxyConfigModel::setTag(const QString &tag) {
setData(index(0), tag, TagRole);
}
void MtProxyConfigModel::setPublicHost(const QString &host) {
const QString t = host.trimmed();
if (!isValidPublicHost(t)) {
return;
}
setData(index(0), t, PublicHostRole);
}
void MtProxyConfigModel::setTransportMode(const QString &mode) {
setData(index(0), mode, TransportModeRole);
}
QString MtProxyConfigModel::getTransportMode() const {
return m_protocolConfig.transportMode.isEmpty()
? QString(protocols::mtProxy::transportModeStandard)
: m_protocolConfig.transportMode;
}
QString MtProxyConfigModel::getTlsDomain() const {
return m_protocolConfig.tlsDomain.isEmpty()
? QString(protocols::mtProxy::defaultTlsDomain)
: m_protocolConfig.tlsDomain;
}
QString MtProxyConfigModel::getPublicHost() const {
return m_protocolConfig.publicHost;
}
void MtProxyConfigModel::setTlsDomain(const QString &domain) {
const QString t = domain.trimmed();
if (!isValidFakeTlsDomain(t)) {
return;
}
setData(index(0), t, TlsDomainRole);
}
void MtProxyConfigModel::setWorkersMode(const QString &mode) {
setData(index(0), mode, WorkersModeRole);
}
void MtProxyConfigModel::setWorkers(const QString &workers) {
setData(index(0), workers, WorkersRole);
}
void MtProxyConfigModel::setNatEnabled(bool enabled) {
setData(index(0), enabled, NatEnabledRole);
}
void MtProxyConfigModel::setNatInternalIp(const QString &ip) {
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatInternalIpRole);
}
void MtProxyConfigModel::setNatExternalIp(const QString &ip) {
const QString t = ip.trimmed();
if (!isValidOptionalIpv4(t)) {
return;
}
setData(index(0), t, NatExternalIpRole);
}
void MtProxyConfigModel::addAdditionalSecret() {
QString newSecret;
for (int i = 0; i < 16; ++i) {
quint32 byte = QRandomGenerator::global()->bounded(256);
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
}
m_protocolConfig.additionalSecrets.append(newSecret);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
void MtProxyConfigModel::removeAdditionalSecret(int idx) {
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
return;
}
m_protocolConfig.additionalSecrets.removeAt(idx);
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
}
QVariantList MtProxyConfigModel::additionalSecretsList() const {
QVariantList out;
out.reserve(m_protocolConfig.additionalSecrets.size());
for (const auto &s: m_protocolConfig.additionalSecrets) {
if (!s.isEmpty()) {
out.append(s);
}
}
return out;
}
void MtProxyConfigModel::setEnabled(bool enabled) {
m_protocolConfig.isEnabled = enabled;
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
}
QString MtProxyConfigModel::generateQrCode(const QString &text) {
if (text.isEmpty()) {
return "";
}
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
}
QString MtProxyConfigModel::defaultTlsDomain() const {
return protocols::mtProxy::defaultTlsDomain;
}
QString MtProxyConfigModel::defaultPort() const {
return protocols::mtProxy::defaultPort;
}
QString MtProxyConfigModel::defaultWorkers() const {
return protocols::mtProxy::defaultWorkers;
}
int MtProxyConfigModel::maxWorkers() const {
return protocols::mtProxy::maxWorkers;
}
QString MtProxyConfigModel::transportModeStandard() const {
return protocols::mtProxy::transportModeStandard;
}
QString MtProxyConfigModel::transportModeFakeTLS() const {
return protocols::mtProxy::transportModeFakeTLS;
}
QString MtProxyConfigModel::workersModeAuto() const {
return protocols::mtProxy::workersModeAuto;
}
QString MtProxyConfigModel::workersModeManual() const {
return protocols::mtProxy::workersModeManual;
}
bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
const QString t = host.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv4Protocol) {
return NetworkUtilities::checkIPv4Format(t);
}
if (a.protocol() == QHostAddress::IPv6Protocol) {
return true;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
return NetworkUtilities::domainRegExp().exactMatch(t);
}
bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const {
return mtproxyPublicHostInputAllowed(text);
}
bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (isValidPublicHost(t)) {
return false;
}
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
if (onlyDigitDot.match(t).hasMatch()) {
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() < 4) {
return true;
}
for (const QString &part: parts) {
if (part.isEmpty()) {
return true;
}
}
return false;
}
if (t.contains(QLatin1Char(':'))) {
if (t.contains(QLatin1String(":::"))) {
return false;
}
if (t.endsWith(QLatin1Char(':'))) {
return true;
}
QHostAddress a(t);
if (a.protocol() == QHostAddress::IPv6Protocol) {
return false;
}
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
return false;
}
bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const {
if (tag.isEmpty()) {
return true;
}
static const QRegularExpression re(
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength));
return re.match(tag).hasMatch();
}
bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
if (!hexOnly.match(t).hasMatch()) {
return false;
}
return t.size() < protocols::mtProxy::botTagHexLength;
}
int MtProxyConfigModel::mtProxyBotTagHexLength() const {
return protocols::mtProxy::botTagHexLength;
}
bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
const QString t = domain.trimmed();
if (t.isEmpty()) {
return true;
}
if (t.length() > 253) {
return false;
}
QHostAddress addr;
if (addr.setAddress(t)) {
return false;
}
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
if (onlyAsciiDigits.match(t).hasMatch()) {
return false;
}
QRegExp re(NetworkUtilities::domainRegExp());
re.setCaseSensitivity(Qt::CaseInsensitive);
if (!re.exactMatch(t)) {
return false;
}
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
if (t.toUtf8().size() > 111) {
return false;
}
return true;
}
QString MtProxyConfigModel::clipboardText() const {
if (QClipboard *c = QGuiApplication::clipboard()) {
return c->text();
}
return QString();
}
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
const QString t = normalizeFakeTlsDomainInput(input);
QString out;
out.reserve(t.size());
for (const QChar &c: t) {
const ushort u = c.unicode();
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
const bool digit = (u >= '0' && u <= '9');
if (letter || digit || u == '.' || u == '-') {
out.append(c);
}
}
if (out.size() > 253) {
out.truncate(253);
}
return out;
}
bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
if (text.length() > 253) {
return false;
}
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
return re.match(text).hasMatch();
}
QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const {
QString out;
const int cap = qMin(input.size(), 253);
out.reserve(cap);
for (const QChar &c: input) {
if (out.size() >= 253) {
break;
}
const ushort u = c.unicode();
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
u == '-') {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 5));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 5) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
QString trimmed = input.trimmed();
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
trimmed = trimmed.mid(2).trimmed();
}
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
const QRegularExpressionMatch m = runHex.match(trimmed);
if (m.hasMatch()) {
return m.captured(1);
}
const int cap = protocols::mtProxy::botTagHexLength;
QString out;
out.reserve(qMin(trimmed.size(), cap));
for (const QChar &c: trimmed) {
if (out.size() >= cap) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 3));
for (const QChar &c: input) {
const ushort u = c.unicode();
if (u >= '0' && u <= '9' && out.size() < 3) {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
QString out;
out.reserve(qMin(input.size(), 15));
for (const QChar &c: input) {
if (out.size() >= 15) {
break;
}
const ushort u = c.unicode();
if ((u >= '0' && u <= '9') || u == '.') {
out.append(c);
}
}
return out;
}
QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
QString t = input.trimmed();
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
t = t.mid(8);
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
t = t.mid(7);
}
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
t = t.left(slash);
}
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
t = t.mid(at + 1);
}
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
t = t.left(colon);
}
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
const QString rest = t.mid(4);
if (rest.contains(QLatin1Char('.'))) {
t = rest;
}
}
return t.trimmed();
}
bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
const QString t = text.trimmed();
if (t.isEmpty()) {
return true;
}
if (isValidFakeTlsDomain(t)) {
return false;
}
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|| t.contains(QLatin1Char(' '))) {
return false;
}
if (t.contains(QLatin1String(".."))) {
return false;
}
if (!t.contains(QLatin1Char('.'))) {
return true;
}
if (t.endsWith(QLatin1Char('.'))) {
return true;
}
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
if (!legalPartial.match(t).hasMatch()) {
return false;
}
return true;
}
bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const {
const QString t = ip.trimmed();
if (t.isEmpty()) {
return true;
}
return NetworkUtilities::checkIPv4Format(t);
}
QHash<int, QByteArray> MtProxyConfigModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[PortRole] = "port";
roles[SecretRole] = "secret";
roles[TagRole] = "tag";
roles[TgLinkRole] = "tgLink";
roles[TmeLinkRole] = "tmeLink";
roles[IsEnabledRole] = "isEnabled";
roles[PublicHostRole] = "publicHost";
roles[TransportModeRole] = "transportMode";
roles[TlsDomainRole] = "tlsDomain";
roles[AdditionalSecretsRole] = "additionalSecrets";
roles[WorkersModeRole] = "workersMode";
roles[WorkersRole] = "workers";
roles[NatEnabledRole] = "natEnabled";
roles[NatInternalIpRole] = "natInternalIp";
roles[NatExternalIpRole] = "natExternalIp";
return roles;
}
amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() {
return m_protocolConfig;
}
@@ -0,0 +1,156 @@
#ifndef MTPROXYCONFIGMODEL_H
#define MTPROXYCONFIGMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QRandomGenerator>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
class MtProxyConfigModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
PortRole = Qt::UserRole + 1,
SecretRole,
TagRole,
TgLinkRole,
TmeLinkRole,
IsEnabledRole,
PublicHostRole,
TransportModeRole,
TlsDomainRole,
AdditionalSecretsRole,
WorkersModeRole,
WorkersRole,
NatEnabledRole,
NatInternalIpRole,
NatExternalIpRole
};
explicit MtProxyConfigModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig);
void updateModel(const QJsonObject &config);
QJsonObject getConfig();
amnezia::MtProxyProtocolConfig getProtocolConfig();
Q_INVOKABLE void generateSecret();
Q_INVOKABLE void setSecret(const QString &secret);
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
Q_INVOKABLE void setPort(const QString &port);
Q_INVOKABLE void setTag(const QString &tag);
Q_INVOKABLE void setPublicHost(const QString &host);
Q_INVOKABLE void setTransportMode(const QString &mode);
Q_INVOKABLE QString getTransportMode() const;
Q_INVOKABLE QString getTlsDomain() const;
Q_INVOKABLE QString getPublicHost() const;
Q_INVOKABLE void setTlsDomain(const QString &domain);
Q_INVOKABLE void setWorkersMode(const QString &mode);
Q_INVOKABLE void setWorkers(const QString &workers);
Q_INVOKABLE void setNatEnabled(bool enabled);
Q_INVOKABLE void setNatInternalIp(const QString &ip);
Q_INVOKABLE void setNatExternalIp(const QString &ip);
Q_INVOKABLE void addAdditionalSecret();
Q_INVOKABLE void removeAdditionalSecret(int idx);
/// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds).
Q_INVOKABLE QVariantList additionalSecretsList() const;
Q_INVOKABLE QString generateQrCode(const QString &text);
Q_INVOKABLE void setEnabled(bool enabled);
Q_INVOKABLE QString defaultTlsDomain() const;
Q_INVOKABLE QString defaultPort() const;
Q_INVOKABLE QString defaultWorkers() const;
Q_INVOKABLE int maxWorkers() const;
Q_INVOKABLE QString transportModeStandard() const;
Q_INVOKABLE QString transportModeFakeTLS() const;
Q_INVOKABLE QString workersModeAuto() const;
Q_INVOKABLE QString workersModeManual() const;
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
Q_INVOKABLE int mtProxyBotTagHexLength() const;
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const;
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
Q_INVOKABLE QString clipboardText() const;
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
amnezia::DockerContainer m_container;
QJsonObject m_fullConfig;
amnezia::MtProxyProtocolConfig m_protocolConfig;
};
#endif // MTPROXYCONFIGMODEL_H
@@ -0,0 +1,127 @@
#include "mtproxy_public_host_input.h"
#include <QRegularExpression>
namespace {
bool ipv4OctetTokenOk(const QString &s) {
static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)"));
if (!re.match(s).hasMatch()) {
return false;
}
bool ok = false;
const int n = s.toInt(&ok);
return ok && n >= 0 && n <= 255;
}
// Reject labels like "312edweqwe" (digits >255 then letters).
bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) {
static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)"));
const QRegularExpressionMatch m = re.match(label);
if (!m.hasMatch()) {
return false;
}
const QString digits = m.captured(1);
if (digits.length() > 3) {
return true;
}
bool ok = false;
const int n = digits.toInt(&ok);
if (!ok) {
return true;
}
if (n > 255) {
return true;
}
// Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below.
return false;
}
// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45").
bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) {
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() != 2) {
return false;
}
if (!ipv4OctetTokenOk(parts.at(0))) {
return false;
}
const QString &p1 = parts.at(1);
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
if (!digitThenLetter.match(p1).hasMatch()) {
return false;
}
return !ipv4OctetTokenOk(p1);
}
// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe").
bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) {
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
if (parts.size() != 4) {
return false;
}
for (int i = 0; i < 3; ++i) {
if (!ipv4OctetTokenOk(parts.at(i))) {
return false;
}
}
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
return digitThenLetter.match(parts.at(3)).hasMatch();
}
bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) {
if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) {
return false;
}
if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) {
return false;
}
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
for (const QString &part: parts) {
if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) {
return false;
}
}
return true;
}
} // namespace
bool mtproxyPublicHostInputAllowed(const QString &text) {
if (text.length() > 253) {
return false;
}
static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)"));
if (!allowed.match(text).hasMatch()) {
return false;
}
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
if (onlyDigits.match(text).hasMatch() && text.length() > 3) {
return false;
}
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) {
static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)"));
return ipv4Partial.match(text).hasMatch();
}
if (text.contains(QLatin1Char(':'))) {
static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)"));
if (!ipv6Chars.match(text).hasMatch()) {
return false;
}
if (text.size() > 45) {
return false;
}
}
if (!hostLabelsRejectBrokenDigitLetterMix(text)) {
return false;
}
return true;
}
PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {}
QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos)
return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid;
}
@@ -0,0 +1,20 @@
#ifndef MTPROXY_PUBLIC_HOST_INPUT_H
#define MTPROXY_PUBLIC_HOST_INPUT_H
#include <QString>
#include <QValidator>
/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII).
bool mtproxyPublicHostInputAllowed(const QString &text);
class PublicHostInputValidator : public QValidator {
Q_OBJECT
public:
explicit PublicHostInputValidator(QObject *parent = nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
#endif
@@ -45,6 +45,9 @@ ListViewType {
PageController.goToPage(PageEnum.PageProtocolRaw)
} else if (isDns) {
PageController.goToPage(PageEnum.PageServiceDnsSettings)
} else if (isMtProxy) {
MtProxyConfigModel.updateModel(config)
PageController.goToPage(PageEnum.PageServiceMtProxySettings)
} else {
InstallController.updateProtocols(ServersUiController.getServerId(ServersUiController.processedServerIndex), containerIndex)
PageController.goToPage(PageEnum.PageSettingsServerProtocol)
@@ -31,6 +31,9 @@ ListViewType {
function triggerCurrentItem() {
var item = root.itemAtIndex(selectedIndex)
if (!item) {
return
}
item.selectable.clicked()
}
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -132,9 +132,11 @@ PageType {
onInstallationErrorOccurred(message)
}
function onUpdateContainerFinished(message) {
function onUpdateContainerFinished(message, closePage) {
PageController.showNotificationMessage(message)
PageController.closePage()
if (closePage) {
PageController.closePage()
}
}
function onCachedProfileCleared(message) {
+1
View File
@@ -78,6 +78,7 @@
<file>Pages2/PageProtocolWireGuardSettings.qml</file>
<file>Pages2/PageProtocolXraySettings.qml</file>
<file>Pages2/PageServiceDnsSettings.qml</file>
<file>Pages2/PageServiceMtProxySettings.qml</file>
<file>Pages2/PageServiceSftpSettings.qml</file>
<file>Pages2/PageServiceSocksProxySettings.qml</file>
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>