diff --git a/client/core/controllers/selfhosted/installController.cpp b/client/core/controllers/selfhosted/installController.cpp index f87f6dbd2c..a2fd57787b 100644 --- a/client/core/controllers/selfhosted/installController.cpp +++ b/client/core/controllers/selfhosted/installController.cpp @@ -103,7 +103,7 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials bool isUpdate) { qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container); - SshSession sshSession(this); + SshSession sshSession; ErrorCode e = ErrorCode::NoError; e = isUserInSudo(credentials, sshSession); @@ -168,11 +168,11 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC } if (container == DockerContainer::MtProxy) { ServerCredentials credentials = adminConfig->credentials(); - SshSession sshSession(this); + SshSession sshSession; MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); } else if (container == DockerContainer::Telemt) { ServerCredentials credentials = adminConfig->credentials(); - SshSession sshSession(this); + SshSession sshSession; TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig); } adminConfig->updateContainerConfig(container, newConfig); @@ -188,7 +188,7 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; bool reinstallRequired = isReinstallContainerRequired(container, oldConfig, newConfig); qDebug() << "InstallController::updateServerConfig for container" << container << "reinstall required is" << reinstallRequired; @@ -211,6 +211,12 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC if (errorCode == ErrorCode::NoError) { errorCode = startupContainerWorker(credentials, container, newConfig, sshSession); } + + if (errorCode == ErrorCode::NoError + && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) { + const QString containerName = ContainerUtils::containerToString(container); + errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName); + } } const bool skipXrayInboundSync = @@ -737,18 +743,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container, if (oldPort != newPort) { return true; } - const QString oldTransport = oldMt->transportMode.isEmpty() ? QString( - protocols::mtProxy::transportModeStandard) - : oldMt->transportMode; - const QString newTransport = newMt->transportMode.isEmpty() ? QString( - protocols::mtProxy::transportModeStandard) - : newMt->transportMode; - if (oldTransport != newTransport) { - return true; - } - if (oldMt->tlsDomain != newMt->tlsDomain) { - return true; - } } } @@ -763,39 +757,6 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container, if (oldPort != newPort) { return true; } - const QString oldTransport = oldT->transportMode.isEmpty() - ? QString(protocols::telemt::transportModeStandard) - : oldT->transportMode; - const QString newTransport = newT->transportMode.isEmpty() - ? QString(protocols::telemt::transportModeStandard) - : newT->transportMode; - if (oldTransport != newTransport) { - return true; - } - if (oldT->tlsDomain != newT->tlsDomain) { - return true; - } - if (oldT->maskEnabled != newT->maskEnabled) { - return true; - } - if (oldT->tlsEmulation != newT->tlsEmulation) { - return true; - } - if (oldT->useMiddleProxy != newT->useMiddleProxy) { - return true; - } - if (oldT->tag != newT->tag) { - return true; - } - const QString oldUser = oldT->userName.isEmpty() - ? QString::fromUtf8(protocols::telemt::defaultUserName) - : oldT->userName; - const QString newUser = newT->userName.isEmpty() - ? QString::fromUtf8(protocols::telemt::defaultUserName) - : newT->userName; - if (oldUser != newUser) { - return true; - } } } @@ -835,6 +796,20 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden qDebug().noquote() << "InstallController::installDockerWorker" << stdOut; + if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) { + QString conntrackOut; + auto cbConntrack = [&](const QString &data, libssh::Client &) { + conntrackOut += data + "\n"; + return ErrorCode::NoError; + }; + sshSession.runScript( + credentials, + sshSession.replaceVars(amnezia::scriptData(SharedScriptType::install_conntrack), + amnezia::genBaseVars(credentials, DockerContainer::None, QString(), QString())), + cbConntrack, cbConntrack); + qDebug().noquote() << "InstallController::installDockerWorker install_conntrack:" << conntrackOut; + } + if (container == DockerContainer::Awg2) { QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)"); QRegularExpressionMatch match = regex.match(stdOut); @@ -970,7 +945,7 @@ ErrorCode InstallController::rebootServer(const QString &serverId) if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; QString script = QString("sudo reboot"); @@ -998,7 +973,7 @@ ErrorCode InstallController::removeAllContainers(const QString &serverId) if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; ErrorCode errorCode = sshSession.runScript(credentials, amnezia::scriptData(SharedScriptType::remove_all_containers)); if (errorCode == ErrorCode::NoError) { @@ -1020,7 +995,7 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; const amnezia::ScriptVars removeContainerVars = amnezia::genBaseVars(credentials, container, QString(), QString()); const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt); @@ -1129,7 +1104,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; QMap installedContainers; ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession); @@ -1172,7 +1147,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser ErrorCode InstallController::installServer(const ServerCredentials &credentials, DockerContainer container, int port, TransportProto transportProto, bool &wasContainerInstalled) { - SshSession sshSession(this); + SshSession sshSession; QMap installedContainers; ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession); if (errorCode) { @@ -1241,7 +1216,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; QMap installedContainers; ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession); @@ -1283,7 +1258,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output, std::function passphraseCallback) { - SshSession sshSession(this); + SshSession sshSession; ErrorCode errorCode = ErrorCode::NoError; if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) { @@ -1564,7 +1539,7 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve return ErrorCode::InternalError; } const QString containerName = ContainerUtils::containerToString(container); - SshSession sshSession(this); + SshSession sshSession; const QString script = enabled ? QStringLiteral("sudo docker start %1").arg(containerName) : QStringLiteral("sudo docker stop %1").arg(containerName); const ErrorCode runError = sshSession.runScript(credentials, script); @@ -1604,7 +1579,7 @@ ErrorCode InstallController::queryDockerContainerStatus(const QString &serverId, stdOut += data; return ErrorCode::NoError; }; - SshSession sshSession(this); + SshSession sshSession; const QString script = QStringLiteral( "sudo docker inspect --format '{{.State.Status}}' %1 2>/dev/null || echo 'not_found'") .arg(containerName); @@ -1638,7 +1613,7 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do if (!credentials.isValid()) { return ErrorCode::InternalError; } - SshSession sshSession(this); + SshSession sshSession; return MtProxyInstaller::queryDiagnostics(sshSession, credentials, container, listenPort, out); } @@ -1661,7 +1636,7 @@ QString InstallController::fetchDockerContainerSecret(const QString &serverId, D stdOut += data; return ErrorCode::NoError; }; - SshSession sshSession(this); + SshSession sshSession; const QString path = QStringLiteral("/data/secret"); const QString cmd = QStringLiteral("sudo docker exec %1 cat %2").arg(containerName, path); const ErrorCode errorCode = sshSession.runScript(credentials, cmd, cbReadStdOut); diff --git a/client/core/installers/mtProxyInstaller.cpp b/client/core/installers/mtProxyInstaller.cpp index 937dab0dbe..149bac8866 100644 --- a/client/core/installers/mtProxyInstaller.cpp +++ b/client/core/installers/mtProxyInstaller.cpp @@ -71,48 +71,62 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve DockerContainer container, int listenPort, MtProxyContainerDiagnostics &out) { - out = {}; - if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) { - return ErrorCode::InternalError; - } - const QString containerName = ContainerUtils::containerToString(container); - const QString script = - QStringLiteral( - "PORT_OK=$(sudo docker exec %1 sh -c 'ss -tlnp 2>/dev/null | grep -q :%2 && echo yes || echo no' 2>/dev/null || echo no); " - "TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%%{http_code}' https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || echo no); " - "CLIENTS=$(sudo docker exec amnezia-mtproxy sh -c 'curl -s --max-time 3 http://localhost:2398/stats 2>/dev/null | grep -o \"total_special_connections:[0-9]*\" | cut -d: -f2' 2>/dev/null); " - "CONF_TIME=$(sudo docker exec amnezia-mtproxy sh -c 'stat -c \"%%y\" /data/proxy-multi.conf 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); " - "echo \"PORT_OK=${PORT_OK}\"; " - "echo \"TG_OK=${TG_OK}\"; " - "echo \"CLIENTS=${CLIENTS:-0}\"; " - "echo \"CONF_TIME=${CONF_TIME}\"; " - "echo \"STATS=http://localhost:2398/stats\";") - .arg(containerName) - .arg(listenPort); - - QString stdOut; - auto cbReadStdOut = [&](const QString &data, libssh::Client &) { - stdOut += data; - return ErrorCode::NoError; - }; - const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut); - if (errorCode != ErrorCode::NoError) { - return errorCode; - } - for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) { - if (line.startsWith(QLatin1String("PORT_OK="))) { - out.portReachable = line.mid(8).trimmed() == QLatin1String("yes"); - } else if (line.startsWith(QLatin1String("TG_OK="))) { - out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes"); - } else if (line.startsWith(QLatin1String("CLIENTS="))) { - out.clientsConnected = line.mid(8).trimmed().toInt(); - } else if (line.startsWith(QLatin1String("CONF_TIME="))) { - out.lastConfigRefresh = line.mid(10).trimmed(); - } else if (line.startsWith(QLatin1String("STATS="))) { - out.statsEndpoint = line.mid(6).trimmed(); + out = { }; + if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) { + const QString containerName = ContainerUtils::containerToString(container); + const bool isTelemt = container == DockerContainer::Telemt; + + const QString sportFilter = QString::number(listenPort); + const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter + + QStringLiteral(" 2>/dev/null | grep ESTABLISHED | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print " + "substr($i,5); break}}'"); + const QString publicFilter = QStringLiteral(" | grep -vE " + "'^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[" + "01])\\.|::1$|fe80:|f[cd][0-9a-f][0-9a-f]:)'"); + const QString clientsCmd = + QStringLiteral("CLIENTS=$(") + peersCmd + publicFilter + QStringLiteral(" | sort -u | grep -c .); "); + const QString confFile = + isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf"); + const QString statsUrl = QString(); + + const QString script = QStringLiteral("CN=") + containerName + QStringLiteral("; ") + + QStringLiteral("PORT_OK=$(sudo ss -tlnp 2>/dev/null | grep -q :") + QString::number(listenPort) + + QStringLiteral(" && echo yes || echo no); ") + + QStringLiteral("TG_OK=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' " + "https://core.telegram.org/getProxySecret 2>/dev/null | grep -q '200' && echo yes || " + "echo no); ") + + clientsCmd + QStringLiteral("CONF_TIME=$(sudo docker exec \"$CN\" sh -c 'stat -c \"%y\" ") + confFile + + QStringLiteral(" 2>/dev/null | cut -d. -f1' 2>/dev/null || echo unknown); ") + + QStringLiteral("echo \"PORT_OK=${PORT_OK}\"; ") + QStringLiteral("echo \"TG_OK=${TG_OK}\"; ") + + QStringLiteral("echo \"CLIENTS=${CLIENTS:-0}\"; ") + QStringLiteral("echo \"CONF_TIME=${CONF_TIME}\"; ") + + QStringLiteral("echo \"STATS=") + statsUrl + QStringLiteral("\";"); + + QString stdOut; + auto cbReadStdOut = [&](const QString &data, libssh::Client &) { + stdOut += data; + return ErrorCode::NoError; + }; + const ErrorCode errorCode = sshSession.runScript(credentials, script, cbReadStdOut); + if (errorCode != ErrorCode::NoError) { + return errorCode; } + for (const QString &line : stdOut.split('\n', Qt::SkipEmptyParts)) { + if (line.startsWith(QLatin1String("PORT_OK="))) { + out.portReachable = line.mid(8).trimmed() == QLatin1String("yes"); + } else if (line.startsWith(QLatin1String("TG_OK="))) { + out.upstreamReachable = line.mid(6).trimmed() == QLatin1String("yes"); + } else if (line.startsWith(QLatin1String("CLIENTS="))) { + out.clientsConnected = line.mid(8).trimmed().toInt(); + } else if (line.startsWith(QLatin1String("CONF_TIME="))) { + out.lastConfigRefresh = line.mid(10).trimmed(); + } else if (line.startsWith(QLatin1String("STATS="))) { + out.statsEndpoint = line.mid(6).trimmed(); + } + } + return ErrorCode::NoError; } - return ErrorCode::NoError; + + return ErrorCode::InternalError; } void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials, diff --git a/client/core/utils/constants/protocolConstants.h b/client/core/utils/constants/protocolConstants.h index 5c65d881e1..b4f57e2184 100644 --- a/client/core/utils/constants/protocolConstants.h +++ b/client/core/utils/constants/protocolConstants.h @@ -271,6 +271,7 @@ namespace amnezia constexpr char workersModeAuto[] = "auto"; constexpr char workersModeManual[] = "manual"; constexpr int maxWorkers = 32; + constexpr int botTagHexLength = 32; } } // namespace protocols diff --git a/client/core/utils/selfhosted/scriptsRegistry.cpp b/client/core/utils/selfhosted/scriptsRegistry.cpp index 4e07ae7522..9c661c2ad6 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.cpp +++ b/client/core/utils/selfhosted/scriptsRegistry.cpp @@ -50,6 +50,7 @@ QString amnezia::scriptName(SharedScriptType type) switch (type) { case SharedScriptType::prepare_host: return QLatin1String("prepare_host.sh"); case SharedScriptType::install_docker: return QLatin1String("install_docker.sh"); + case SharedScriptType::install_conntrack: return QLatin1String("install_conntrack.sh"); case SharedScriptType::build_container: return QLatin1String("build_container.sh"); case SharedScriptType::remove_container: return QLatin1String("remove_container.sh"); case SharedScriptType::remove_all_containers: return QLatin1String("remove_all_containers.sh"); @@ -366,6 +367,14 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi vars.append({ { "$TELEMT_USE_MIDDLE_PROXY", c.useMiddleProxy ? QLatin1String("true") : QLatin1String("false") } }); vars.append({ { "$TELEMT_MASK", c.maskEnabled ? QLatin1String("true") : QLatin1String("false") } }); vars.append({ { "$TELEMT_TLS_EMULATION", c.tlsEmulation ? QLatin1String("true") : QLatin1String("false") } }); + + QStringList additionalList; + for (const QString &s : c.additionalSecrets) { + if (!s.isEmpty()) { + additionalList << s; + } + } + vars.append({ { "$TELEMT_ADDITIONAL_SECRETS", additionalList.join(QLatin1Char(',')) } }); } return vars; diff --git a/client/core/utils/selfhosted/scriptsRegistry.h b/client/core/utils/selfhosted/scriptsRegistry.h index f63b850a6e..a1e5d202df 100644 --- a/client/core/utils/selfhosted/scriptsRegistry.h +++ b/client/core/utils/selfhosted/scriptsRegistry.h @@ -21,6 +21,7 @@ enum SharedScriptType { // General scripts prepare_host, install_docker, + install_conntrack, build_container, remove_container, remove_all_containers, diff --git a/client/server_scripts/install_conntrack.sh b/client/server_scripts/install_conntrack.sh new file mode 100644 index 0000000000..c372fee08b --- /dev/null +++ b/client/server_scripts/install_conntrack.sh @@ -0,0 +1,10 @@ +if command -v conntrack > /dev/null 2>&1; then echo "conntrack already installed"; exit 0; fi;\ +if which apt-get > /dev/null 2>&1; then pm=$(which apt-get); silent_inst="-yq install --install-recommends"; check_pkgs="-yq update"; conntrack_pkg="conntrack"; dist="debian";\ +elif which dnf > /dev/null 2>&1; then pm=$(which dnf); silent_inst="-yq install"; check_pkgs="-yq check-update"; conntrack_pkg="conntrack-tools"; dist="fedora";\ +elif which yum > /dev/null 2>&1; then pm=$(which yum); silent_inst="-y -q install"; check_pkgs="-y -q check-update"; conntrack_pkg="conntrack-tools"; dist="centos";\ +elif which zypper > /dev/null 2>&1; then pm=$(which zypper); silent_inst="-nq install"; check_pkgs="-nq refresh"; conntrack_pkg="conntrack-tools"; dist="opensuse";\ +elif which pacman > /dev/null 2>&1; then pm=$(which pacman); silent_inst="-S --noconfirm --noprogressbar --quiet"; check_pkgs="-Sup"; conntrack_pkg="conntrack-tools"; dist="archlinux";\ +else echo "Packet manager not found"; exit 0; fi;\ +if [ "$dist" = "debian" ]; then export DEBIAN_FRONTEND=noninteractive; fi;\ +sudo $pm $check_pkgs; sudo $pm $silent_inst $conntrack_pkg;\ +command -v conntrack > /dev/null 2>&1 && echo "conntrack installed" || echo "conntrack install failed" diff --git a/client/server_scripts/serverScripts.qrc b/client/server_scripts/serverScripts.qrc index 278e169536..2d1ae60598 100644 --- a/client/server_scripts/serverScripts.qrc +++ b/client/server_scripts/serverScripts.qrc @@ -18,6 +18,7 @@ dns/Dockerfile dns/run_container.sh install_docker.sh + install_conntrack.sh ipsec/configure_container.sh ipsec/Dockerfile ipsec/mobileconfig.plist diff --git a/client/server_scripts/telemt/configure_container.sh b/client/server_scripts/telemt/configure_container.sh index 8cfcf5f77f..4a5c24ce8e 100644 --- a/client/server_scripts/telemt/configure_container.sh +++ b/client/server_scripts/telemt/configure_container.sh @@ -61,6 +61,12 @@ rm -f /data/config.toml echo "" echo "[access.users]" echo "$TELEMT_USER_NAME = \"$SECRET\"" + i=1 + for EXTRA in $(echo "$TELEMT_ADDITIONAL_SECRETS" | tr ',' ' '); do + echo "$EXTRA" | grep -qE '^[0-9a-fA-F]{32}$' || continue + echo "extra_$i = \"$EXTRA\"" + i=$((i + 1)) + done } > /data/config.toml echo "$SECRET" > /data/secret diff --git a/client/ui/controllers/selfhosted/installUiController.cpp b/client/ui/controllers/selfhosted/installUiController.cpp index 3b8b8513f5..b145f7c32e 100644 --- a/client/ui/controllers/selfhosted/installUiController.cpp +++ b/client/ui/controllers/selfhosted/installUiController.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "core/utils/api/apiUtils.h" #include "core/controllers/selfhosted/installController.h" @@ -359,17 +360,27 @@ void InstallUiController::setContainerEnabled(const QString &serverId, int conta } 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); + InstallController *installController = m_installController; + auto *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, + [this, watcher, serverId, container, enabled]() { + const ErrorCode errorCode = watcher->result(); + watcher->deleteLater(); + 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); + }); + QFuture future = QtConcurrent::run([installController, serverId, container, enabled]() -> ErrorCode { + return installController->setDockerContainerEnabledState(serverId, container, enabled); + }); + watcher->setFuture(future); } void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex) @@ -379,13 +390,23 @@ void InstallUiController::refreshContainerStatus(const QString &serverId, int co return; } - int status = 3; - const ErrorCode errorCode = m_installController->queryDockerContainerStatus(serverId, container, status); - if (errorCode != ErrorCode::NoError) { - emit containerStatusRefreshed(3); - return; - } - emit containerStatusRefreshed(status); + using StatusResult = std::pair; // {status, errorCode} + InstallController *installController = m_installController; + auto *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { + const StatusResult result = watcher->result(); + watcher->deleteLater(); + emit containerStatusRefreshed(result.first, result.second); + }); + QFuture future = QtConcurrent::run([installController, serverId, container]() -> StatusResult { + int status = 3; + const ErrorCode errorCode = installController->queryDockerContainerStatus(serverId, container, status); + if (errorCode != ErrorCode::NoError) { + return { 3, static_cast(errorCode) }; + } + return { status, static_cast(ErrorCode::NoError) }; + }); + watcher->setFuture(future); } void InstallUiController::refreshContainerDiagnostics(const QString &serverId, int containerIndex, int port) @@ -395,14 +416,27 @@ void InstallUiController::refreshContainerDiagnostics(const QString &serverId, i return; } - 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); + using DiagResult = std::pair; + InstallController *installController = m_installController; + auto *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { + const DiagResult result = watcher->result(); + watcher->deleteLater(); + if (!result.first) { + emit containerDiagnosticsRefreshed(false, false, -1, QString(), QString()); + return; + } + const MtProxyContainerDiagnostics &diag = result.second; + emit containerDiagnosticsRefreshed(diag.portReachable, diag.upstreamReachable, diag.clientsConnected, + diag.lastConfigRefresh, diag.statsEndpoint); + }); + QFuture future = + QtConcurrent::run([installController, serverId, container, port]() -> DiagResult { + MtProxyContainerDiagnostics diag; + const ErrorCode errorCode = installController->queryMtProxyDiagnostics(serverId, container, port, diag); + return { errorCode == ErrorCode::NoError, diag }; + }); + watcher->setFuture(future); } void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex) @@ -412,8 +446,17 @@ void InstallUiController::fetchContainerSecret(const QString &serverId, int cont return; } - const QString secret = m_installController->fetchDockerContainerSecret(serverId, container); - emit containerSecretFetched(secret); + InstallController *installController = m_installController; + auto *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { + const QString secret = watcher->result(); + watcher->deleteLater(); + emit containerSecretFetched(secret); + }); + QFuture future = QtConcurrent::run([installController, serverId, container]() -> QString { + return installController->fetchDockerContainerSecret(serverId, container); + }); + watcher->setFuture(future); } void InstallUiController::rebootServer(const QString &serverId) diff --git a/client/ui/controllers/selfhosted/installUiController.h b/client/ui/controllers/selfhosted/installUiController.h index 25c1fed4d5..14d506b720 100644 --- a/client/ui/controllers/selfhosted/installUiController.h +++ b/client/ui/controllers/selfhosted/installUiController.h @@ -114,7 +114,7 @@ public slots: void removeAllContainersFinished(const QString &finishedMessage); void removeContainerFinished(const QString &finishedMessage); void setContainerEnabledFinished(bool enabled); - void containerStatusRefreshed(int status); + void containerStatusRefreshed(int status, int errorCode); void containerDiagnosticsRefreshed(bool portReachable, bool upstreamReachable, int clientsConnected, const QString &lastConfigRefresh, const QString &statsEndpoint); void containerSecretFetched(const QString &secret); diff --git a/client/ui/models/services/mtProxyConfigModel.cpp b/client/ui/models/services/mtProxyConfigModel.cpp index 5e68d786a4..5221a52384 100644 --- a/client/ui/models/services/mtProxyConfigModel.cpp +++ b/client/ui/models/services/mtProxyConfigModel.cpp @@ -8,8 +8,6 @@ #include "core/utils/constants/configKeys.h" #include "qrcodegen.hpp" -#include -#include #include #include #include @@ -332,7 +330,7 @@ void MtProxyConfigModel::removeAdditionalSecret(int idx) { QVariantList MtProxyConfigModel::additionalSecretsList() const { QVariantList out; out.reserve(m_protocolConfig.additionalSecrets.size()); - for (const auto &s: m_protocolConfig.additionalSecrets) { + for (const auto &s : m_protocolConfig.additionalSecrets) { if (!s.isEmpty()) { out.append(s); } @@ -398,6 +396,9 @@ bool MtProxyConfigModel::isValidPublicHost(const QString &host) const { return NetworkUtilities::checkIPv4Format(t); } if (a.protocol() == QHostAddress::IPv6Protocol) { + if (a.isNull() || a.isLoopback() || a == QHostAddress(QHostAddress::AnyIPv6)) { + return false; + } return true; } static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)")); @@ -504,20 +505,12 @@ bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const { 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; @@ -578,7 +571,6 @@ QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) co 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()) { @@ -599,18 +591,6 @@ QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) co 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)); diff --git a/client/ui/models/services/mtProxyConfigModel.h b/client/ui/models/services/mtProxyConfigModel.h index b67969ed40..968e5d2418 100644 --- a/client/ui/models/services/mtProxyConfigModel.h +++ b/client/ui/models/services/mtProxyConfigModel.h @@ -128,16 +128,12 @@ public slots: 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; diff --git a/client/ui/models/services/telemtConfigModel.cpp b/client/ui/models/services/telemtConfigModel.cpp index 6a3fd9eb10..57af1d9fc3 100644 --- a/client/ui/models/services/telemtConfigModel.cpp +++ b/client/ui/models/services/telemtConfigModel.cpp @@ -1,7 +1,13 @@ #include "telemtConfigModel.h" +#include "ui/models/utils/mtproxy_public_host_input.h" + +#include +#include #include +#include +#include "core/utils/networkUtilities.h" #include "core/utils/qrCodeUtils.h" #include "core/utils/constants/configKeys.h" #include "core/utils/constants/protocolConstants.h" @@ -9,7 +15,9 @@ using namespace amnezia; -TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) {} +TelemtConfigModel::TelemtConfigModel(QObject *parent) : QAbstractListModel(parent) { + qmlRegisterType("TelemtConfig", 1, 0, "PublicHostInputValidator"); +} void TelemtConfigModel::applyDefaults(TelemtProtocolConfig &c) { if (c.port.isEmpty()) { @@ -49,7 +57,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, break; } case Roles::TagRole: { - m_protocolConfig.tag = value.toString(); + const QString tag = sanitizeMtProxyTagFieldText(value.toString()); + if (!isValidMtProxyTag(tag)) { + return false; + } + m_protocolConfig.tag = tag; break; } case Roles::IsEnabledRole: { @@ -57,7 +69,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, break; } case Roles::PublicHostRole: { - m_protocolConfig.publicHost = value.toString(); + const QString h = value.toString().trimmed(); + if (!isValidPublicHost(h)) { + return false; + } + m_protocolConfig.publicHost = h; break; } case Roles::TransportModeRole: { @@ -65,7 +81,11 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, break; } case Roles::TlsDomainRole: { - m_protocolConfig.tlsDomain = value.toString(); + const QString d = value.toString().trimmed(); + if (!isValidFakeTlsDomain(d)) { + return false; + } + m_protocolConfig.tlsDomain = d; break; } case Roles::AdditionalSecretsRole: { @@ -85,11 +105,19 @@ bool TelemtConfigModel::setData(const QModelIndex &index, const QVariant &value, break; } case Roles::NatInternalIpRole: { - m_protocolConfig.natInternalIp = value.toString(); + const QString ip = value.toString().trimmed(); + if (!isValidOptionalIpv4(ip)) { + return false; + } + m_protocolConfig.natInternalIp = ip; break; } case Roles::NatExternalIpRole: { - m_protocolConfig.natExternalIp = value.toString(); + const QString ip = value.toString().trimmed(); + if (!isValidOptionalIpv4(ip)) { + return false; + } + m_protocolConfig.natExternalIp = ip; break; } case Roles::MaskEnabledRole: { @@ -238,7 +266,7 @@ void TelemtConfigModel::setSecret(const QString &secret) { } bool TelemtConfigModel::validateAndSetSecret(const QString &rawSecret) { - if (!QRegularExpression(QStringLiteral("^[0-9a-fA-F]{32}$")).match(rawSecret).hasMatch()) { + if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) { return false; } setData(index(0), rawSecret, SecretRole); @@ -254,7 +282,11 @@ void TelemtConfigModel::setTag(const QString &tag) { } void TelemtConfigModel::setPublicHost(const QString &host) { - setData(index(0), host, PublicHostRole); + const QString t = host.trimmed(); + if (!isValidPublicHost(t)) { + return; + } + setData(index(0), t, PublicHostRole); } void TelemtConfigModel::setTransportMode(const QString &mode) { @@ -262,13 +294,15 @@ void TelemtConfigModel::setTransportMode(const QString &mode) { } QString TelemtConfigModel::getTransportMode() const { - return m_protocolConfig.transportMode.isEmpty() ? QString::fromUtf8(protocols::telemt::transportModeStandard) - : m_protocolConfig.transportMode; + return m_protocolConfig.transportMode.isEmpty() + ? QString(protocols::telemt::transportModeStandard) + : m_protocolConfig.transportMode; } QString TelemtConfigModel::getTlsDomain() const { - return m_protocolConfig.tlsDomain.isEmpty() ? QString::fromUtf8(protocols::telemt::defaultTlsDomain) - : m_protocolConfig.tlsDomain; + return m_protocolConfig.tlsDomain.isEmpty() + ? QString(protocols::telemt::defaultTlsDomain) + : m_protocolConfig.tlsDomain; } QString TelemtConfigModel::getPublicHost() const { @@ -276,7 +310,11 @@ QString TelemtConfigModel::getPublicHost() const { } void TelemtConfigModel::setTlsDomain(const QString &domain) { - setData(index(0), domain, TlsDomainRole); + const QString t = domain.trimmed(); + if (!isValidFakeTlsDomain(t)) { + return; + } + setData(index(0), t, TlsDomainRole); } void TelemtConfigModel::setWorkersMode(const QString &mode) { @@ -292,11 +330,19 @@ void TelemtConfigModel::setNatEnabled(bool enabled) { } void TelemtConfigModel::setNatInternalIp(const QString &ip) { - setData(index(0), ip, NatInternalIpRole); + const QString t = ip.trimmed(); + if (!isValidOptionalIpv4(t)) { + return; + } + setData(index(0), t, NatInternalIpRole); } void TelemtConfigModel::setNatExternalIp(const QString &ip) { - setData(index(0), ip, NatExternalIpRole); + const QString t = ip.trimmed(); + if (!isValidOptionalIpv4(t)) { + return; + } + setData(index(0), t, NatExternalIpRole); } void TelemtConfigModel::setMaskEnabled(bool enabled) { @@ -334,6 +380,17 @@ void TelemtConfigModel::removeAdditionalSecret(int idx) { emit dataChanged(index(0), index(0), QList{AdditionalSecretsRole}); } +QVariantList TelemtConfigModel::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 TelemtConfigModel::setEnabled(bool enabled) { m_protocolConfig.isEnabled = enabled; emit dataChanged(index(0), index(0), QList{IsEnabledRole}); @@ -348,15 +405,15 @@ QString TelemtConfigModel::generateQrCode(const QString &text) { } QString TelemtConfigModel::defaultTlsDomain() const { - return QString::fromUtf8(protocols::telemt::defaultTlsDomain); + return protocols::telemt::defaultTlsDomain; } QString TelemtConfigModel::defaultPort() const { - return QString::fromUtf8(protocols::telemt::defaultPort); + return protocols::telemt::defaultPort; } QString TelemtConfigModel::defaultWorkers() const { - return QString::fromUtf8(protocols::telemt::defaultWorkers); + return protocols::telemt::defaultWorkers; } int TelemtConfigModel::maxWorkers() const { @@ -364,19 +421,303 @@ int TelemtConfigModel::maxWorkers() const { } QString TelemtConfigModel::transportModeStandard() const { - return QString::fromUtf8(protocols::telemt::transportModeStandard); + return protocols::telemt::transportModeStandard; } QString TelemtConfigModel::transportModeFakeTLS() const { - return QString::fromUtf8(protocols::telemt::transportModeFakeTLS); + return protocols::telemt::transportModeFakeTLS; } QString TelemtConfigModel::workersModeAuto() const { - return QString::fromUtf8(protocols::telemt::workersModeAuto); + return protocols::telemt::workersModeAuto; } QString TelemtConfigModel::workersModeManual() const { - return QString::fromUtf8(protocols::telemt::workersModeManual); + return protocols::telemt::workersModeManual; +} + +bool TelemtConfigModel::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) { + if (a.isNull() || a.isLoopback() || a == QHostAddress(QHostAddress::AnyIPv6)) { + return false; + } + return true; + } + static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)")); + if (onlyAsciiDigits.match(t).hasMatch()) { + return false; + } + return NetworkUtilities::domainRegExp().exactMatch(t); +} + +bool TelemtConfigModel::isPublicHostInputAllowed(const QString &text) const { + return mtproxyPublicHostInputAllowed(text); +} + +bool TelemtConfigModel::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 TelemtConfigModel::isValidMtProxyTag(const QString &tag) const { + if (tag.isEmpty()) { + return true; + } + static const QRegularExpression re( + QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::telemt::botTagHexLength)); + return re.match(tag).hasMatch(); +} + +bool TelemtConfigModel::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::telemt::botTagHexLength; +} + +int TelemtConfigModel::mtProxyBotTagHexLength() const { + return protocols::telemt::botTagHexLength; +} + +bool TelemtConfigModel::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; + } + if (t.toUtf8().size() > 111) { + return false; + } + return true; +} + +QString TelemtConfigModel::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 TelemtConfigModel::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 TelemtConfigModel::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 TelemtConfigModel::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; +} + +QString TelemtConfigModel::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 TelemtConfigModel::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 TelemtConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const { + QString trimmed = input.trimmed(); + if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) { + trimmed = trimmed.mid(2).trimmed(); + } + 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::telemt::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 TelemtConfigModel::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; +} + +bool TelemtConfigModel::isValidOptionalIpv4(const QString &ip) const { + const QString t = ip.trimmed(); + if (t.isEmpty()) { + return true; + } + return NetworkUtilities::checkIPv4Format(t); } QHash TelemtConfigModel::roleNames() const { diff --git a/client/ui/models/services/telemtConfigModel.h b/client/ui/models/services/telemtConfigModel.h index c386d210e3..1ba38a8a72 100644 --- a/client/ui/models/services/telemtConfigModel.h +++ b/client/ui/models/services/telemtConfigModel.h @@ -88,6 +88,8 @@ public slots: Q_INVOKABLE void removeAdditionalSecret(int idx); + Q_INVOKABLE QVariantList additionalSecretsList() const; + Q_INVOKABLE QString generateQrCode(const QString &text); Q_INVOKABLE void setEnabled(bool enabled); @@ -116,12 +118,44 @@ public slots: 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 bool isFakeTlsDomainTypingIncomplete(const QString &text) const; + + Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const; + + Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) 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 sanitizeOptionalIpv4FieldText(const QString &input) const; + + Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const; + protected: QHash roleNames() const override; private: static void applyDefaults(amnezia::TelemtProtocolConfig &c); + QString normalizeFakeTlsDomainInput(const QString &input) const; + amnezia::DockerContainer m_container = amnezia::DockerContainer::None; QJsonObject m_fullConfig; amnezia::TelemtProtocolConfig m_protocolConfig; diff --git a/client/ui/qml/Pages2/PageServiceMtProxySettings.qml b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml index 03b6c0272a..532857649a 100644 --- a/client/ui/qml/Pages2/PageServiceMtProxySettings.qml +++ b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml @@ -16,11 +16,11 @@ import "../Controls2/TextTypes" import "../Config" import "../Components" - PageType { id: root property int containerStatus: 1 + property int statusErrorCode: 0 property bool isUpdating: false property bool isCheckingStatus: false property bool isFetchingSecret: false @@ -261,6 +261,7 @@ PageType { isCheckingStatus = false isFetchingSecret = false busyIndicatorShown = false + statusErrorCode = 0 PageController.disableControls(false) PageController.showBusyIndicator(false) diagLoading = false @@ -348,13 +349,18 @@ PageType { enabled ? qsTr("MTProxy started") : qsTr("MTProxy stopped")) } - function onContainerStatusRefreshed(status) { + function onContainerStatusRefreshed(status, errorCode) { if (!root.visible) { isCheckingStatus = false isFetchingSecret = false return } containerStatus = status + root.statusErrorCode = errorCode + if (status === 3 && errorCode !== 0) { + PageController.showNotificationMessage( + qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(errorCode)) + } root.savedTransportMode = MtProxyConfigModel.getTransportMode() root.savedTlsDomain = MtProxyConfigModel.getTlsDomain() @@ -402,1524 +408,1595 @@ PageType { anchors.fill: parent enabled: !root.pageBusy - BackButtonType { - id: backButton - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 + PageController.safeAreaTopMargin - - onFocusChanged: { - if (this.activeFocus) { - if (mainTabBar.currentIndex === 0) { - connectionListView.positionViewAtBeginning() - } else { - settingsListView.positionViewAtBeginning() + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + + onFocusChanged: { + if (this.activeFocus) { + if (mainTabBar.currentIndex === 0) { + connectionListView.positionViewAtBeginning() + } else { + settingsListView.positionViewAtBeginning() + } } } } - } - - ColumnLayout { - id: pageHeader - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 - - BaseHeaderType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 24 - - headerText: qsTr("MTProxy settings") - descriptionLinkText: qsTr("Read more about this settings") - descriptionLinkUrl: "https://core.telegram.org/proxy" - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 8 - visible: root.mtProxyNetworkBlocked - text: qsTr("No internet connection. Connect to the internet to change MTProxy settings.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 14 - } - } - TabBar { - id: mainTabBar - anchors.top: pageHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - width: parent.width - - background: Rectangle { - color: AmneziaStyle.color.transparent - Rectangle { - width: parent.width - height: 1 - anchors.bottom: parent.bottom - color: AmneziaStyle.color.slateGray + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 + + headerText: qsTr("MTProxy settings") + descriptionLinkText: qsTr("Read more about this settings") + descriptionLinkUrl: "https://core.telegram.org/proxy" } - } - TabButtonType { - text: qsTr("Connection") - isSelected: mainTabBar.currentIndex === 0 - } - TabButtonType { - text: qsTr("Settings") - isSelected: mainTabBar.currentIndex === 1 + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + visible: root.mtProxyNetworkBlocked + text: qsTr("No internet connection. Connect to the internet to change MTProxy settings.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } } - } - - StackLayout { - id: tabContent - anchors.top: mainTabBar.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - currentIndex: mainTabBar.currentIndex - - ListViewType { - id: connectionListView - model: MtProxyConfigModel - - delegate: ColumnLayout { - width: connectionListView.width - spacing: 0 - property int secretTabIndex: root.syncedSecretTabIndex + TabBar { + id: mainTabBar + anchors.top: pageHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + width: parent.width - function activeSecret() { - return root.mtProxyClientSecretForTabIndex(secret, root.syncedSecretTabIndex, - root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) - } - - function effectiveSecret() { - return activeSecret() - } - - function effectiveHost() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } - - function tmeLink() { - return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - function tgLink() { - return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray } + } - CaptionTextType { - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: linkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 + StackLayout { + id: tabContent + anchors.top: mainTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex - RowLayout { - id: linkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 + ListViewType { + id: connectionListView + model: MtProxyConfigModel - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? tmeLink() : qsTr("Deploy MTProxy first") - color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } + delegate: ColumnLayout { + width: connectionListView.width + spacing: 0 - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - ExportController.generateQrFromString(tmeLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") - } - } + property int secretTabIndex: root.syncedSecretTabIndex - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - GC.copyToClipBoard(tmeLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } + function activeSecret() { + return root.mtProxyClientSecretForTabIndex(secret, root.syncedSecretTabIndex, + root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: tgLinkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - visible: secret !== "" - - RowLayout { - id: tgLinkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - CaptionTextType { - Layout.fillWidth: true - text: tgLink() - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(tgLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") - } - } + function effectiveSecret() { + return activeSecret() + } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(tgLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) } - } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 4 + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } - CaptionTextType { - text: qsTr("Or enter the proxy details manually.") - color: AmneziaStyle.color.mutedGray + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() } CaptionTextType { Layout.fillWidth: true - text: qsTr("How to do it") - color: AmneziaStyle.color.goldenApricot - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") - } + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray } - Item { + Rectangle { Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 32 - implicitHeight: manualCol.implicitHeight + 8 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - ColumnLayout { - id: manualCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - spacing: 0 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Host") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: effectiveHost() - color: AmneziaStyle.color.paleGray - elide: Text.ElideRight + text: secret !== "" ? tmeLink() : qsTr("Deploy MTProxy first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") } } + ImageButtonType { implicitWidth: 36 implicitHeight: 36 hoverEnabled: true image: "qrc:/images/controls/copy.svg" imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(effectiveHost()) - PageController.showNotificationMessage(qsTr("Copied")) } + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } } } + } - DividerType { - Layout.fillWidth: true - } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Port") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: port - color: AmneziaStyle.color.paleGray + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") } } + ImageButtonType { implicitWidth: 36 implicitHeight: 36 hoverEnabled: true image: "qrc:/images/controls/copy.svg" imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(port) - PageController.showNotificationMessage(qsTr("Copied")) } + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } } } + } - DividerType { - Layout.fillWidth: true + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray } - ButtonGroup { - id: secretTabGroup + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } } - RowLayout { + Item { Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 4 - Layout.bottomMargin: 8 - ColumnLayout { + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + spacing: 0 + + RowLayout { Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } } - CaptionTextType { + } + + DividerType { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { Layout.fillWidth: true - text: activeSecret() - color: AmneziaStyle.color.paleGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 13 + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } } } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(activeSecret()) - PageController.showNotificationMessage(qsTr("Copied")) } + + DividerType { + Layout.fillWidth: true + } + + ButtonGroup { + id: secretTabGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } } } - } - LabelWithButtonType { - id: removeButton - Layout.fillWidth: true - Layout.bottomMargin: 24 - Layout.leftMargin: 0 - Layout.rightMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - text: qsTr("Delete MTProxy") - textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function () { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) + LabelWithButtonType { + id: removeButton + Layout.fillWidth: true + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + text: qsTr("Delete MTProxy") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { - }) - } - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false } } } - } - ListViewType { - id: settingsListView - model: MtProxyConfigModel - reuseItems: false + ListViewType { + id: settingsListView + model: MtProxyConfigModel + reuseItems: false - delegate: ColumnLayout { - id: settingsRoot - width: settingsListView.width - spacing: 0 + delegate: ColumnLayout { + id: settingsRoot + width: settingsListView.width + spacing: 0 - function mtProxyActiveSecretForBaseHex(baseHex) { - return root.mtProxyClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, - root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) - } + readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy - function mtProxyEffectiveHostForLinks() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } + function mtProxyActiveSecretForBaseHex(baseHex) { + return root.mtProxyClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, + root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) + } - function mtProxyTmeLinkForAdditional(baseHex) { - return "https://t.me/proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) - } + function mtProxyEffectiveHostForLinks() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) + } - function mtProxyTgLinkForAdditional(baseHex) { - return "tg://proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) - } + function mtProxyTmeLinkForAdditional(baseHex) { + return "https://t.me/proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } - SwitcherType { - id: enableMtProxySwitch - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Enable MTProxy") - checked: isEnabled - enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy - && !root.mtProxyNetworkBlocked - onToggled: function () { - if (checked !== isEnabled) { - previousEnabled = isEnabled - previousContainerStatus = containerStatus - root.previousSecret = secret - isEnabled = checked - isUpdating = true - if (checked) { - root.pendingUpdateAfterEnable = true - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) - } else { - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) + function mtProxyTgLinkForAdditional(baseHex) { + return "tg://proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } + + function mtProxyIsAdditionalPersisted(hex) { + return root.mtProxyIsPersistedAdditionalHex(hex) + } + + function mtProxyCopyText(text) { + GC.copyToClipBoard(text) + PageController.showNotificationMessage(qsTr("Copied")) + } + + function mtProxyShareQr(link) { + ExportController.generateQrFromString(link) + PageController.goToShareConnectionPage(qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), "", "", "") + } + + function mtProxyRemoveAdditionalSecret(idx) { + MtProxyConfigModel.removeAdditionalSecret(idx) + } + + SwitcherType { + id: enableMtProxySwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable MTProxy") + checked: isEnabled + enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy + && !root.mtProxyNetworkBlocked + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + root.previousSecret = secret + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) + } } } } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 * 2 - spacing: 4 CaptionTextType { - text: qsTr("Base secret") + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: !fieldsEditable && !root.pageBusy + text: (containerStatus === 1 || containerStatus === 2) + ? qsTr("Enable MTProxy to edit settings") + : (statusErrorCode !== 0 + ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) + : qsTr("Cannot reach the server — settings are unavailable")) color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 + wrapMode: Text.WordWrap } - RowLayout { + ColumnLayout { Layout.fillWidth: true - spacing: 8 + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? mtProxyActiveSecretForBaseHex(secret) : qsTr("Not generated") - color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 14 + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 } - ImageButtonType { - Layout.alignment: Qt.AlignTop - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: AmneziaStyle.color.paleGray - visible: ServersUiController.isProcessedServerHasWriteAccess() - onClicked: { - var secretSnapshot = secret - showQuestionDrawer( - qsTr("Generate new secret?"), - qsTr("All existing connection links will stop working. Users will need new links."), - qsTr("Generate"), - qsTr("Cancel"), - function () { - root.previousSecret = secretSnapshot - if (containerStatus === 1) { - isUpdating = true - MtProxyConfigModel.generateSecret() - root.mtProxyScheduleUpdate(false) - } else { - MtProxyConfigModel.generateSecret() - PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when MTProxy is started.")) + RowLayout { + Layout.fillWidth: true + spacing: 8 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? mtProxyActiveSecretForBaseHex(secret) : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 14 + } + + ImageButtonType { + Layout.alignment: Qt.AlignTop + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable + onClicked: { + var secretSnapshot = secret + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + root.previousSecret = secretSnapshot + if (containerStatus === 1) { + isUpdating = true + MtProxyConfigModel.generateSecret() + root.mtProxyScheduleUpdate(false) + } else { + MtProxyConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when MTProxy is started.")) + } + }, + function () { } - }, - function () { - } - ) + ) + } } } } - } - TextFieldWithHeaderType { - id: publicHostTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - headerText: qsTr("Public host / IP") - textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) - textField.text: publicHost - textField.maximumLength: 253 - textField.validator: PublicHostInputValidator { - } - textField.onTextChanged: { - var t = publicHostTextField.textField.text - if (MtProxyConfigModel.isPublicHostTypingIncomplete(t)) { - publicHostTextField.errorText = "" - } else if (!MtProxyConfigModel.isValidPublicHost(t)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - } else { - publicHostTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidPublicHost(textField.text)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - return - } - publicHostTextField.errorText = "" - if (textField.text !== publicHost) { - publicHost = textField.text - MtProxyConfigModel.setPublicHost(publicHost) - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - visible: publicHostTextField.textField.text === "" - text: qsTr("Leave empty to use server IP automatically") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: publicHostTextField.textField.text !== "" && - publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) - text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: portTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Server port") - textField.placeholderText: MtProxyConfigModel.defaultPort() - textField.maximumLength: 5 - textField.validator: IntValidator { - bottom: 1 - top: 65535 - } - Component.onCompleted: { - var savedPort = port - textField.text = (savedPort === MtProxyConfigModel.defaultPort()) ? "" : savedPort - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" - text: qsTr("FakeTLS may not work on ports other than 443") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: tagTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("MTProxy bot tag (optional)") - textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") - textField.text: tag - textField.maximumLength: MtProxyConfigModel.mtProxyBotTagHexLength() - textField.onTextChanged: { - var cur = tagTextField.textField.text - var clean = MtProxyConfigModel.sanitizeMtProxyTagFieldText(cur) - if (clean !== cur) { - textField.text = clean - textField.cursorPosition = clean.length - return - } - var tt = tagTextField.textField.text - if (tt === "") { - tagTextField.errorText = "" - return - } - if (MtProxyConfigModel.isMtProxyTagTypingIncomplete(tt)) { - tagTextField.errorText = "" - return - } - if (!MtProxyConfigModel.isValidMtProxyTag(tt)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") - return + TextFieldWithHeaderType { + id: publicHostTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) + textField.text: publicHost + textField.maximumLength: 253 + textField.validator: PublicHostInputValidator { } - tagTextField.errorText = "" - } - textField.onEditingFinished: { - var raw = textField.text.replace(/^\s+|\s+$/g, '') - var normalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(raw) - textField.text = normalized - if (!MtProxyConfigModel.isValidMtProxyTag(normalized)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") - return + textField.onTextChanged: { + var t = publicHostTextField.textField.text + if (MtProxyConfigModel.isPublicHostTypingIncomplete(t)) { + publicHostTextField.errorText = "" + } else if (!MtProxyConfigModel.isValidPublicHost(t)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + } else { + publicHostTextField.errorText = "" + } } - tagTextField.errorText = "" - if (normalized !== tag) { - tag = normalized - MtProxyConfigModel.setTag(tag) + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidPublicHost(textField.text)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + return + } + publicHostTextField.errorText = "" + if (textField.text !== publicHost) { + publicHost = textField.text + MtProxyConfigModel.setPublicHost(publicHost) + } } } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 CaptionTextType { - text: qsTr("Get a tag from") + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") color: AmneziaStyle.color.mutedGray font.pixelSize: 12 + wrapMode: Text.WordWrap } + CaptionTextType { - text: "@MTProxyBot" + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") color: AmneziaStyle.color.goldenApricot font.pixelSize: 12 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") - } + wrapMode: Text.WordWrap } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - text: qsTr("Transport mode") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - DropDownType { - id: transportModeDropDown - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - - drawerParent: root - drawerHeight: 0.4 - headerText: qsTr("Transport mode") - text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") - - listView: Component { - ListViewType { - model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] - delegate: LabelWithButtonType { - Layout.fillWidth: true - text: modelData - rightImageSource: { - var isCurrent = (index === 0 && transportMode === "standard") || - (index === 1 && transportMode === "faketls") - return isCurrent ? "qrc:/images/controls/check.svg" : "" - } - rightImageColor: AmneziaStyle.color.goldenApricot - clickedFunction: function () { - transportMode = (index === 0) ? "standard" : "faketls" - MtProxyConfigModel.setTransportMode(transportMode) - root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 - transportModeDropDown.closeTriggered() - } - } + TextFieldWithHeaderType { + id: portTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: MtProxyConfigModel.defaultPort() + textField.maximumLength: 5 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 1 + top: 65535 } - } - } - - TextFieldWithHeaderType { - id: tlsDomainTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: transportMode === "faketls" - headerText: qsTr("FakeTLS domain") - textField.placeholderText: root.previousTlsDomain - Component.onCompleted: { - var savedDomain = tlsDomain - textField.text = (savedDomain === MtProxyConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - var domainValue = textField.text === "" ? MtProxyConfigModel.defaultTlsDomain() : textField.text - if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValue)) { - tlsDomainTextField.errorText = qsTr("Enter a valid domain name") - return + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === MtProxyConfigModel.defaultPort()) ? "" : savedPort + } + textField.onTextChanged: { + var cur = portTextField.textField.text + var clean = MtProxyConfigModel.sanitizePortFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + } } - tlsDomainTextField.errorText = "" - if (domainValue !== tlsDomain) { - tlsDomain = domainValue - MtProxyConfigModel.setTlsDomain(tlsDomain) + textField.onEditingFinished: { + textField.text = MtProxyConfigModel.sanitizePortFieldText(textField.text) } } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - visible: transportMode === "faketls" CaptionTextType { Layout.fillWidth: true - text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot font.pixelSize: 12 + wrapMode: Text.WordWrap } + CaptionTextType { Layout.fillWidth: true - text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") - color: AmneziaStyle.color.goldenApricot - wrapMode: Text.WordWrap + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") + color: AmneziaStyle.color.mutedGray font.pixelSize: 12 + wrapMode: Text.WordWrap } - } - LabelWithButtonType { - id: advancedHeader - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 16 - property bool expanded: false - text: qsTr("Advanced") - rightImageSource: expanded - ? "qrc:/images/controls/chevron-up.svg" - : "qrc:/images/controls/chevron-down.svg" - rightImageColor: AmneziaStyle.color.mutedGray - clickedFunction: function () { - expanded = !expanded + TextFieldWithHeaderType { + id: tagTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") + textField.text: tag + textField.maximumLength: MtProxyConfigModel.mtProxyBotTagHexLength() + textField.onTextChanged: { + var cur = tagTextField.textField.text + var clean = MtProxyConfigModel.sanitizeMtProxyTagFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + return + } + var tt = tagTextField.textField.text + if (tt === "") { + tagTextField.errorText = "" + return + } + if (MtProxyConfigModel.isMtProxyTagTypingIncomplete(tt)) { + tagTextField.errorText = "" + return + } + if (!MtProxyConfigModel.isValidMtProxyTag(tt)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") + return + } + tagTextField.errorText = "" + } + textField.onEditingFinished: { + var raw = textField.text.replace(/^\s+|\s+$/g, '') + var normalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(raw) + textField.text = normalized + if (!MtProxyConfigModel.isValidMtProxyTag(normalized)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") + return + } + tagTextField.errorText = "" + if (normalized !== tag) { + tag = normalized + MtProxyConfigModel.setTag(tag) + } + } } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - visible: advancedHeader.expanded - CaptionTextType { + RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.topMargin: 8 - Layout.bottomMargin: 4 - text: qsTr("Additional secrets") - color: AmneziaStyle.color.mutedGray + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } } + CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") - color: AmneziaStyle.color.charcoalGray - wrapMode: Text.WordWrap + Layout.topMargin: 16 * 2 + text: qsTr("Transport mode") + color: AmneziaStyle.color.mutedGray font.pixelSize: 12 } - Repeater { - model: additionalSecrets - delegate: ColumnLayout { - id: addSecretDelegate - property bool linksExpanded: false - readonly property bool linksPanelAllowed: root.mtProxyIsPersistedAdditionalHex(modelData) - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 0 + DropDownType { + id: transportModeDropDown + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 - onLinksPanelAllowedChanged: { - if (!linksPanelAllowed) { - linksExpanded = false + drawerParent: root + drawerHeight: 0.4 + headerText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + MtProxyConfigModel.setTransportMode(transportMode) + root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 + transportModeDropDown.closeTriggered() + } } } + } + } - Rectangle { - Layout.fillWidth: true - implicitHeight: collapsedBar.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: collapsedBar - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 8 - - Item { - Layout.fillWidth: true - implicitHeight: Math.max(hexCaption.implicitHeight, 24) - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 8 + TextFieldWithHeaderType { + id: tlsDomainTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + textField.validator: RegularExpressionValidator { + regularExpression: /^[A-Za-z0-9.-]*$/ + } + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === MtProxyConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onTextChanged: { + var t = tlsDomainTextField.textField.text + if (t === "" || MtProxyConfigModel.isFakeTlsDomainTypingIncomplete(t) + || MtProxyConfigModel.isValidFakeTlsDomain(t)) { + tlsDomainTextField.errorText = "" + } else { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? MtProxyConfigModel.defaultTlsDomain() : textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValue)) { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + return + } + tlsDomainTextField.errorText = "" + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + MtProxyConfigModel.setTlsDomain(tlsDomain) + } + } + } - CaptionTextType { - id: hexCaption - Layout.fillWidth: true - text: modelData - color: AmneziaStyle.color.paleGray - elide: Text.ElideMiddle - font.pixelSize: 13 - } + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" - Image { - width: 24 - height: 24 - visible: addSecretDelegate.linksPanelAllowed - source: "qrc:/images/controls/chevron-down.svg" - sourceSize.width: 24 - sourceSize.height: 24 - rotation: addSecretDelegate.linksExpanded ? 180 : 0 - Behavior on rotation { - NumberAnimation { - duration: 150 - } - } - } - } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } - MouseArea { - anchors.fill: parent - visible: addSecretDelegate.linksPanelAllowed - enabled: addSecretDelegate.linksPanelAllowed - cursorShape: Qt.PointingHandCursor - onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded - } - } + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - hoverEnabled: true - visible: ServersUiController.isProcessedServerHasWriteAccess() - image: "qrc:/images/controls/trash.svg" - imageColor: AmneziaStyle.color.vibrantRed - onClicked: { - MtProxyConfigModel.removeAdditionalSecret(index) - if (containerStatus === 1) { - root.mtProxyScheduleUpdate(false) - } - } - } - } - } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + enabled: fieldsEditable - ColumnLayout { + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: ColumnLayout { + id: addSecretDelegate + property bool linksExpanded: false + readonly property bool linksPanelAllowed: settingsRoot.mtProxyIsAdditionalPersisted(modelData) Layout.fillWidth: true - Layout.topMargin: 8 - spacing: 8 - visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 0 + + onLinksPanelAllowedChanged: { + if (!linksPanelAllowed) { + linksExpanded = false + } } Rectangle { Layout.fillWidth: true - implicitHeight: expTmeRow.implicitHeight + 16 + implicitHeight: collapsedBar.implicitHeight + 16 color: AmneziaStyle.color.onyxBlack radius: 8 border.color: AmneziaStyle.color.slateGray border.width: 1 RowLayout { - id: expTmeRow + id: collapsedBar anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 12 anchors.rightMargin: 8 - spacing: 4 + spacing: 8 - CaptionTextType { + Item { Layout.fillWidth: true - text: settingsRoot.mtProxyTmeLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } + implicitHeight: Math.max(hexCaption.implicitHeight, 24) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + CaptionTextType { + id: hexCaption + Layout.fillWidth: true + text: settingsRoot.mtProxyActiveSecretForBaseHex(modelData) + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") + Image { + width: 24 + height: 24 + visible: addSecretDelegate.linksPanelAllowed + source: "qrc:/images/controls/chevron-down.svg" + sourceSize.width: 24 + sourceSize.height: 24 + rotation: addSecretDelegate.linksExpanded ? 180 : 0 + Behavior on rotation { + NumberAnimation { + duration: 150 + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: addSecretDelegate.linksPanelAllowed + enabled: addSecretDelegate.linksPanelAllowed + cursorShape: Qt.PointingHandCursor + onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded } } ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 + implicitWidth: 32 + implicitHeight: 32 hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) - PageController.showNotificationMessage(qsTr("Copied")) - } + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: settingsRoot.mtProxyRemoveAdditionalSecret(index) } } } - Rectangle { + ColumnLayout { Layout.fillWidth: true - implicitHeight: expTgRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 + Layout.topMargin: 8 + spacing: 8 + visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded - RowLayout { - id: expTgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } - CaptionTextType { - Layout.fillWidth: true - text: settingsRoot.mtProxyTgLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } + Rectangle { + Layout.fillWidth: true + implicitHeight: expTmeRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(settingsRoot.mtProxyTgLinkForAdditional(modelData)) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") + RowLayout { + id: expTmeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTmeLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) } } + } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(settingsRoot.mtProxyTgLinkForAdditional(modelData)) - PageController.showNotificationMessage(qsTr("Copied")) + Rectangle { + Layout.fillWidth: true + implicitHeight: expTgRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTgLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTgLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTgLinkForAdditional(modelData)) } } } } } } - } - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 8 + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Add additional secret") - clickedFunc: function () { - MtProxyConfigModel.addAdditionalSecret() + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + MtProxyConfigModel.addAdditionalSecret() + } } - } - - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - LabelTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Worker mode") - } - - ButtonGroup { - id: workerModeGroup - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - spacing: 0 - visible: transportMode !== "faketls" - HorizontalRadioButton { + DividerType { Layout.fillWidth: true - text: qsTr("Auto") - ButtonGroup.group: workerModeGroup - checked: workersMode === "auto" - onClicked: { workersMode = "auto"; MtProxyConfigModel.setWorkersMode("auto") } + Layout.bottomMargin: 8 } - HorizontalRadioButton { + + LabelTextType { Layout.fillWidth: true - text: qsTr("Manual") - ButtonGroup.group: workerModeGroup - checked: workersMode === "manual" - onClicked: { workersMode = "manual"; MtProxyConfigModel.setWorkersMode("manual") } + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - visible: transportMode === "faketls" - text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - TextFieldWithHeaderType { - id: workersTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: workersMode === "manual" && transportMode !== "faketls" - headerText: qsTr("Workers count") - textField.placeholderText: "2" - textField.text: workers - textField.maximumLength: 3 - textField.validator: IntValidator { - bottom: 1 - top: MtProxyConfigModel.maxWorkers() + ButtonGroup { + id: workerModeGroup } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== workers) { - workers = textField.text - MtProxyConfigModel.setWorkers(workers) + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; MtProxyConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; MtProxyConfigModel.setWorkersMode("manual") } } } - } - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } - SwitcherType { - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Server is behind NAT / Docker bridge") - descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") - checked: natEnabled - onToggled: function () { - if (checked !== natEnabled) { - natEnabled = checked - MtProxyConfigModel.setNatEnabled(natEnabled) + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 2 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 0 + top: MtProxyConfigModel.maxWorkers() + } + textField.onTextChanged: { + var cur = workersTextField.textField.text + if (cur === "") { + return + } + var n = parseInt(cur, 10) + var maxW = MtProxyConfigModel.maxWorkers() + if (isNaN(n) || n < 0) { + n = 0 + } + if (n > maxW) { + n = maxW + } + var clamped = String(n) + if (clamped !== cur) { + textField.text = clamped + textField.cursorPosition = clamped.length + } + } + textField.onEditingFinished: { + var v = workersTextField.textField.text + if (v !== "") { + var m = parseInt(v, 10) + var maxW2 = MtProxyConfigModel.maxWorkers() + if (isNaN(m) || m < 0) { + m = 0 + } + if (m > maxW2) { + m = maxW2 + } + v = String(m) + textField.text = v + } + if (v !== workers) { + workers = v + MtProxyConfigModel.setWorkers(workers) + } } } - } - TextFieldWithHeaderType { - id: natInternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("Internal IP") - textField.placeholderText: "172.17.0.2" - textField.text: natInternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natInternalIpTextField.errorText = "" + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + MtProxyConfigModel.setNatEnabled(natEnabled) + } } } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat } - natInternalIpTextField.errorText = "" - if (textField.text !== natInternalIp) { - natInternalIp = textField.text - MtProxyConfigModel.setNatInternalIp(natInternalIp) + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natInternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natInternalIpTextField.errorText = "" + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + MtProxyConfigModel.setNatInternalIp(natInternalIp) + } } } - } - TextFieldWithHeaderType { - id: natExternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("External IP") - textField.placeholderText: "1.2.3.4" - textField.text: natExternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat - } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natExternalIpTextField.errorText = "" + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natExternalIpTextField.errorText = "" + } } - natExternalIpTextField.errorText = "" - if (textField.text !== natExternalIp) { - natExternalIp = textField.text - MtProxyConfigModel.setNatExternalIp(natExternalIp) + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natExternalIpTextField.errorText = "" + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + MtProxyConfigModel.setNatExternalIp(natExternalIp) + } } } } - } - - DividerType { - Layout.fillWidth: true - Layout.topMargin: 8 - } - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 8 - visible: containerStatus === 1 + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } - RowLayout { + ColumnLayout { Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 - Header2Type { + RowLayout { Layout.fillWidth: true - headerText: qsTr("Diagnostics") - } - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray - hoverEnabled: !diagLoading - enabled: !diagLoading - onClicked: { - diagLoading = true - InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") } - } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + } + } } - CaptionTextType { + + RowLayout { Layout.fillWidth: true - text: qsTr("Public port reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - CaptionTextType { + RowLayout { Layout.fillWidth: true - text: qsTr("Telegram upstream reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - } - CaptionTextType { + RowLayout { Layout.fillWidth: true - text: qsTr("Clients connected") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() - color: AmneziaStyle.color.paleGray + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: AmneziaStyle.color.mutedGray + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } } - CaptionTextType { + + LabelWithButtonType { Layout.fillWidth: true - text: qsTr("Last config refresh") - color: AmneziaStyle.color.paleGray + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } } + CaptionTextType { - text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") color: AmneziaStyle.color.mutedGray - } - } - - LabelWithButtonType { - Layout.fillWidth: true - Layout.leftMargin: -16 - visible: diagStatsEndpoint !== "" - text: qsTr("Stats endpoint") - descriptionText: diagStatsEndpoint - descriptionOnTop: true - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray - clickedFunction: function () { - GC.copyToClipBoard(diagStatsEndpoint) - PageController.showNotificationMessage(qsTr("Copied")) + visible: diagClientsConnected < 0 } } CaptionTextType { Layout.fillWidth: true - text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") color: AmneziaStyle.color.mutedGray - visible: diagClientsConnected < 0 + wrapMode: Text.WordWrap + font.pixelSize: 12 } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - Layout.bottomMargin: 24 - text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 32 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - enabled: !root.mtProxyNetworkBlocked - text: qsTr("Save") - clickedFunc: function () { - if (root.mtProxyNetworkBlocked) { - PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change MTProxy settings.")) - return - } - publicHostTextField.errorText = "" - tagTextField.errorText = "" - tlsDomainTextField.errorText = "" - natInternalIpTextField.errorText = "" - natExternalIpTextField.errorText = "" - portTextField.errorText = "" - - var portValue = portTextField.textField.text === "" - ? MtProxyConfigModel.defaultPort() - : portTextField.textField.text - - var errorLines = [] - var bullet = "- " - if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { - var portErr = qsTr("The port must be in the range of 1 to 65535") - portTextField.errorText = portErr - errorLines.push(bullet + portErr) - } - if (!MtProxyConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { - var hostErr = qsTr("Enter a valid IP address or domain name") - publicHostTextField.errorText = hostErr - errorLines.push(bullet + hostErr) - } - var tagNormalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) - tagTextField.textField.text = tagNormalized - if (!MtProxyConfigModel.isValidMtProxyTag(tagNormalized)) { - var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") - tagTextField.errorText = tagErr - errorLines.push(bullet + tagErr) - } - var domainValueForSave = tlsDomainTextField.textField.text === "" - ? MtProxyConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValueForSave)) { - var tlsErr = qsTr("Enter a valid domain name") - tlsDomainTextField.errorText = tlsErr - errorLines.push(bullet + tlsErr) - } - var natIpErr = qsTr("Enter a valid IPv4 address") - if (!MtProxyConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { - natInternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) - } - if (!MtProxyConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { - natExternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) - } - if (errorLines.length > 0) { - PageController.showErrorMessage(errorLines.join("\n")) - return - } - MtProxyConfigModel.setPort(portValue) - MtProxyConfigModel.setTag(tagNormalized) - MtProxyConfigModel.setPublicHost(publicHostTextField.textField.text) - MtProxyConfigModel.setTransportMode(transportMode) - var domainValue = tlsDomainTextField.textField.text === "" - ? MtProxyConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - MtProxyConfigModel.setTlsDomain(domainValue) - - if (transportMode === "faketls") { - workers = "0" - MtProxyConfigModel.setWorkers("0") - } else { - MtProxyConfigModel.setWorkersMode(workersMode) - MtProxyConfigModel.setWorkers(workers) + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable && !root.mtProxyNetworkBlocked + text: qsTr("Save") + clickedFunc: function () { + if (root.mtProxyNetworkBlocked) { + PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change MTProxy settings.")) + return + } + publicHostTextField.errorText = "" + tagTextField.errorText = "" + tlsDomainTextField.errorText = "" + natInternalIpTextField.errorText = "" + natExternalIpTextField.errorText = "" + portTextField.errorText = "" + + var portValue = portTextField.textField.text === "" + ? MtProxyConfigModel.defaultPort() + : portTextField.textField.text + + var errorLines = [] + var bullet = "- " + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + var portErr = qsTr("The port must be in the range of 1 to 65535") + portTextField.errorText = portErr + errorLines.push(bullet + portErr) + } + if (!MtProxyConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { + var hostErr = qsTr("Enter a valid IP address or domain name") + publicHostTextField.errorText = hostErr + errorLines.push(bullet + hostErr) + } + var tagNormalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) + tagTextField.textField.text = tagNormalized + if (!MtProxyConfigModel.isValidMtProxyTag(tagNormalized)) { + var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") + tagTextField.errorText = tagErr + errorLines.push(bullet + tagErr) + } + var domainValueForSave = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValueForSave)) { + var tlsErr = qsTr("Enter a valid domain name") + tlsDomainTextField.errorText = tlsErr + errorLines.push(bullet + tlsErr) + } + var natIpErr = qsTr("Enter a valid IPv4 address") + if (!MtProxyConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { + natInternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) + } + if (!MtProxyConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { + natExternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) + } + if (errorLines.length > 0) { + PageController.showErrorMessage(errorLines.join("\n")) + return + } + MtProxyConfigModel.setPort(portValue) + MtProxyConfigModel.setTag(tagNormalized) + MtProxyConfigModel.setPublicHost(publicHostTextField.textField.text) + MtProxyConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + MtProxyConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + MtProxyConfigModel.setWorkers("0") + } else { + MtProxyConfigModel.setWorkersMode(workersMode) + MtProxyConfigModel.setWorkers(workers) + } + MtProxyConfigModel.setNatEnabled(natEnabled) + MtProxyConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + MtProxyConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + root.previousSecret = secret + isUpdating = true + root.mtProxyScheduleUpdate(false) } - MtProxyConfigModel.setNatEnabled(natEnabled) - MtProxyConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) - MtProxyConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) - - previousPort = port - previousTag = tag - previousPublicHost = publicHost - previousTransportMode = transportMode - previousTlsDomain = tlsDomain - previousWorkersMode = workersMode - previousWorkers = workers - previousNatEnabled = natEnabled - previousNatInternalIp = natInternalIp - previousNatExternalIp = natExternalIp - root.previousSecret = secret - isUpdating = true - root.mtProxyScheduleUpdate(false) } } } } } - - } } diff --git a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml index 2cec630d5c..2a73fc48bc 100644 --- a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml @@ -8,6 +8,7 @@ import PageEnum 1.0 import ContainerProps 1.0 import ProtocolEnum 1.0 import Style 1.0 +import TelemtConfig 1.0 import "./" import "../Controls2" @@ -19,6 +20,7 @@ PageType { id: root property int containerStatus: 1 + property int statusErrorCode: 0 property bool isUpdating: false property bool isCheckingStatus: false property bool isFetchingSecret: false @@ -151,6 +153,29 @@ PageType { root.telemtScheduleContainerStatusRefresh() } + property var telemtPersistedAdditionalHex: [] + + function telemtRefreshPersistedAdditionalSecrets() { + var list = TelemtConfigModel.additionalSecretsList() + var a = [] + for (var i = 0; i < list.length; ++i) { + a.push(String(list[i])) + } + root.telemtPersistedAdditionalHex = a + } + + function telemtIsPersistedAdditionalHex(hex) { + var h = String(hex) + for (var j = 0; j < root.telemtPersistedAdditionalHex.length; ++j) { + if (String(root.telemtPersistedAdditionalHex[j]) === h) { + return true + } + } + return false + } + + readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/ + function telemtScheduleUpdate(closePage) { var cp = closePage === undefined ? false : closePage Qt.callLater(function () { @@ -158,6 +183,33 @@ PageType { }) } + function natIpv4FieldShowInvalidError(text) { + var t = text ? String(text).replace(/^\s+|\s+$/g, '') : "" + if (t === "") + return false + if (TelemtConfigModel.isValidOptionalIpv4(t)) + return false + var parts = t.split('.') + var j + for (j = 0; j < parts.length; j++) { + if (parts[j].length > 3) + return true + } + if (parts.length > 4) + return true + if (t.indexOf('.') < 0 && t.length > 3) + return true + if (t.endsWith('.')) + return false + if (parts.length < 4) + return false + for (var i = 0; i < parts.length; i++) { + if (parts[i] === "") + return true + } + return true + } + function statusText() { if (isCheckingStatus) { return qsTr("Checking...") @@ -189,6 +241,10 @@ PageType { root.savedTlsDomain = TelemtConfigModel.getTlsDomain() root.savedPublicHost = TelemtConfigModel.getPublicHost() + Qt.callLater(function () { + root.telemtRefreshPersistedAdditionalSecrets() + }) + Qt.callLater(root.telemtOnPageShown) } @@ -205,6 +261,7 @@ PageType { isCheckingStatus = false isFetchingSecret = false busyIndicatorShown = false + statusErrorCode = 0 PageController.disableControls(false) PageController.showBusyIndicator(false) diagLoading = false @@ -245,10 +302,8 @@ PageType { root.savedTransportMode = TelemtConfigModel.getTransportMode() root.savedTlsDomain = TelemtConfigModel.getTlsDomain() root.savedPublicHost = TelemtConfigModel.getPublicHost() + root.telemtRefreshPersistedAdditionalSecrets() PageController.showNotificationMessage(message) - if (closePage) { - PageController.closePage() - } } function onInstallationErrorOccurred() { @@ -294,13 +349,18 @@ PageType { enabled ? qsTr("Telemt started") : qsTr("Telemt stopped")) } - function onContainerStatusRefreshed(status) { + function onContainerStatusRefreshed(status, errorCode) { if (!root.visible) { isCheckingStatus = false isFetchingSecret = false return } containerStatus = status + root.statusErrorCode = errorCode + if (status === 3 && errorCode !== 0) { + PageController.showNotificationMessage( + qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(errorCode)) + } root.savedTransportMode = TelemtConfigModel.getTransportMode() root.savedTlsDomain = TelemtConfigModel.getTlsDomain() @@ -348,877 +408,692 @@ PageType { anchors.fill: parent enabled: !root.pageBusy - BackButtonType { - id: backButton - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 + PageController.safeAreaTopMargin - - onFocusChanged: { - if (this.activeFocus) { - if (mainTabBar.currentIndex === 0) { - connectionListView.positionViewAtBeginning() - } else { - settingsListView.positionViewAtBeginning() + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin + + onFocusChanged: { + if (this.activeFocus) { + if (mainTabBar.currentIndex === 0) { + connectionListView.positionViewAtBeginning() + } else { + settingsListView.positionViewAtBeginning() + } } } } - } - - ColumnLayout { - id: pageHeader - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 - - BaseHeaderType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 24 - - headerText: qsTr("Telemt settings") - descriptionLinkText: qsTr("Read more about this settings") - descriptionLinkUrl: "https://github.com/telemt/telemt" - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 8 - visible: root.telemtNetworkBlocked - text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 14 - } - } - TabBar { - id: mainTabBar - anchors.top: pageHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - width: parent.width - - background: Rectangle { - color: AmneziaStyle.color.transparent - Rectangle { - width: parent.width - height: 1 - anchors.bottom: parent.bottom - color: AmneziaStyle.color.slateGray + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 + + headerText: qsTr("Telemt settings") + descriptionLinkText: qsTr("Read more about this settings") + descriptionLinkUrl: "https://github.com/telemt/telemt" } - } - TabButtonType { - text: qsTr("Connection") - isSelected: mainTabBar.currentIndex === 0 - } - TabButtonType { - text: qsTr("Settings") - isSelected: mainTabBar.currentIndex === 1 + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + visible: root.telemtNetworkBlocked + text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } } - } - - StackLayout { - id: tabContent - anchors.top: mainTabBar.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - currentIndex: mainTabBar.currentIndex - - ListViewType { - id: connectionListView - model: TelemtConfigModel - - delegate: ColumnLayout { - width: connectionListView.width - spacing: 0 - property int secretTabIndex: root.syncedSecretTabIndex + TabBar { + id: mainTabBar + anchors.top: pageHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + width: parent.width - function activeSecret() { - return root.telemtClientSecretForTabIndex(secret, root.syncedSecretTabIndex, - root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) - } - - function effectiveSecret() { - return activeSecret() - } - - function effectiveHost() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } - - function tmeLink() { - return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - function tgLink() { - return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray } + } - CaptionTextType { - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: linkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 + StackLayout { + id: tabContent + anchors.top: mainTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex - RowLayout { - id: linkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 + ListViewType { + id: connectionListView + model: TelemtConfigModel - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") - color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } + delegate: ColumnLayout { + width: connectionListView.width + spacing: 0 - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - ExportController.generateQrFromString(tmeLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("Telemt connection link"), - "", "", "") - } - } + property int secretTabIndex: root.syncedSecretTabIndex - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - GC.copyToClipBoard(tmeLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } + function activeSecret() { + return root.telemtClientSecretForTabIndex(secret, root.syncedSecretTabIndex, + root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: tgLinkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - visible: secret !== "" - - RowLayout { - id: tgLinkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - CaptionTextType { - Layout.fillWidth: true - text: tgLink() - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(tgLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("Telemt connection link"), - "", "", "") - } - } + function effectiveSecret() { + return activeSecret() + } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(tgLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) } - } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 4 + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } - CaptionTextType { - text: qsTr("Or enter the proxy details manually.") - color: AmneziaStyle.color.mutedGray + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() } CaptionTextType { Layout.fillWidth: true - text: qsTr("How to do it") - color: AmneziaStyle.color.goldenApricot - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") - } + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray } - Item { + Rectangle { Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 32 - implicitHeight: manualCol.implicitHeight + 8 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - ColumnLayout { - id: manualCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - spacing: 0 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Host") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: effectiveHost() - color: AmneziaStyle.color.paleGray - elide: Text.ElideRight - } + text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 } + ImageButtonType { implicitWidth: 36 implicitHeight: 36 hoverEnabled: true - image: "qrc:/images/controls/copy.svg" + image: "qrc:/images/controls/qr-code.svg" imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(effectiveHost()) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - - DividerType { - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Port") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: port - color: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") } } + ImageButtonType { implicitWidth: 36 implicitHeight: 36 hoverEnabled: true image: "qrc:/images/controls/copy.svg" imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(port) - PageController.showNotificationMessage(qsTr("Copied")) } + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } } } + } - DividerType { - Layout.fillWidth: true - } - - ButtonGroup { - id: secretTabGroup - } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 4 - Layout.bottomMargin: 8 - ColumnLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: activeSecret() - color: AmneziaStyle.color.paleGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 13 + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") } } + ImageButtonType { implicitWidth: 36 implicitHeight: 36 hoverEnabled: true image: "qrc:/images/controls/copy.svg" imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(activeSecret()) - PageController.showNotificationMessage(qsTr("Copied")) } + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } } } } - } - - LabelWithButtonType { - id: removeButton - Layout.fillWidth: true - Layout.bottomMargin: 24 - Layout.leftMargin: 0 - Layout.rightMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - text: qsTr("Delete Telemt") - textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function () { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) - } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { - }) - } - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false - } - } - } - } - ListViewType { - id: settingsListView - model: TelemtConfigModel - reuseItems: false - - delegate: ColumnLayout { - width: settingsListView.width - spacing: 0 + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 4 - function telemtActiveSecretForBaseHex(baseHex) { - return root.telemtClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, - root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) - } + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } - SwitcherType { - id: enableTelemtSwitch - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Enable Telemt") - checked: isEnabled - enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy - && !root.telemtNetworkBlocked - onToggled: function () { - if (checked !== isEnabled) { - previousEnabled = isEnabled - previousContainerStatus = containerStatus - root.previousSecret = secret - isEnabled = checked - isUpdating = true - if (checked) { - root.pendingUpdateAfterEnable = true - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) - } else { - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") } } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 * 2 - spacing: 4 - CaptionTextType { - text: qsTr("Base secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 + Item { + Layout.fillWidth: true + } } - RowLayout { + Rectangle { Layout.fillWidth: true - spacing: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? telemtActiveSecretForBaseHex(secret) : qsTr("Not generated") - color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 14 - } + DividerType { + Layout.fillWidth: true + } - ImageButtonType { - Layout.alignment: Qt.AlignTop - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: AmneziaStyle.color.paleGray - visible: ServersUiController.isProcessedServerHasWriteAccess() - onClicked: { - var secretSnapshot = secret - showQuestionDrawer( - qsTr("Generate new secret?"), - qsTr("All existing connection links will stop working. Users will need new links."), - qsTr("Generate"), - qsTr("Cancel"), - function () { - root.previousSecret = secretSnapshot - if (containerStatus === 1) { - isUpdating = true - TelemtConfigModel.generateSecret() - root.telemtScheduleUpdate(false) - } else { - TelemtConfigModel.generateSecret() - PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) - } - }, - function () { + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } + } + } + + DividerType { + Layout.fillWidth: true + } + + ButtonGroup { + id: secretTabGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 } - ) + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } } } - } - TextFieldWithHeaderType { - id: publicHostTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - headerText: qsTr("Public host / IP") - textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) - textField.text: publicHost - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== publicHost) { - publicHost = textField.text - TelemtConfigModel.setPublicHost(publicHost) + LabelWithButtonType { + id: removeButton + Layout.fillWidth: true + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + text: qsTr("Delete Telemt") + textColor: AmneziaStyle.color.vibrantRed + clickedFunction: function () { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false } } } + } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - visible: publicHostTextField.textField.text === "" - text: qsTr("Leave empty to use server IP automatically") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } + ListViewType { + id: settingsListView + model: TelemtConfigModel + reuseItems: false - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: publicHostTextField.textField.text !== "" && - publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) - text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } + delegate: ColumnLayout { + id: settingsRoot + width: settingsListView.width + spacing: 0 + + readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy - TextFieldWithHeaderType { - id: portTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Server port") - textField.placeholderText: TelemtConfigModel.defaultPort() - textField.maximumLength: 5 - textField.validator: IntValidator { - bottom: 1 - top: 65535 + function telemtActiveSecretForBaseHex(baseHex) { + return root.telemtClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, + root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) } - Component.onCompleted: { - var savedPort = port - textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort + + function telemtEffectiveHostForLinks() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - var portValue = textField.text === "" ? TelemtConfigModel.defaultPort() : textField.text - if (portValue !== port) { - port = portValue - TelemtConfigModel.setPort(port) - } + + function telemtTmeLinkForAdditional(baseHex) { + return "https://t.me/proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) } - } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" - text: qsTr("FakeTLS may not work on ports other than 443") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } + function telemtTgLinkForAdditional(baseHex) { + return "tg://proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) + } - TextFieldWithHeaderType { - id: tagTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Promoted channel tag (optional)") - textField.placeholderText: qsTr("leave empty if not needed") - textField.text: tag - textField.maximumLength: 64 - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== tag) { - tag = textField.text - TelemtConfigModel.setTag(tag) - } + function telemtIsAdditionalPersisted(hex) { + return root.telemtIsPersistedAdditionalHex(hex) } - } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 + function telemtCopyText(text) { + GC.copyToClipBoard(text) + PageController.showNotificationMessage(qsTr("Copied")) + } - CaptionTextType { - text: qsTr("Get a tag from") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 + function telemtShareQr(link) { + ExportController.generateQrFromString(link) + PageController.goToShareConnectionPage(qsTr("Telegram connection link"), + qsTr("Telemt connection link"), "", "", "") } - CaptionTextType { - text: "@MTProxyBot" - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") - } + + function telemtRemoveAdditionalSecret(idx) { + TelemtConfigModel.removeAdditionalSecret(idx) } - } - DropDownType { - id: transportModeDropDown - Layout.fillWidth: true - Layout.topMargin: 16 * 2 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - - drawerParent: root - drawerHeight: 0.35 - descriptionText: qsTr("Transport mode") - text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") - - listView: Component { - ListViewType { - model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] - delegate: LabelWithButtonType { - Layout.fillWidth: true - text: modelData - rightImageSource: { - var isCurrent = (index === 0 && transportMode === "standard") || - (index === 1 && transportMode === "faketls") - return isCurrent ? "qrc:/images/controls/check.svg" : "" - } - rightImageColor: AmneziaStyle.color.goldenApricot - clickedFunction: function () { - transportMode = (index === 0) ? "standard" : "faketls" - TelemtConfigModel.setTransportMode(transportMode) - root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 - transportModeDropDown.closeTriggered() + SwitcherType { + id: enableTelemtSwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable Telemt") + checked: isEnabled + enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy + && !root.telemtNetworkBlocked + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + root.previousSecret = secret + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) } } } } - } - - TextFieldWithHeaderType { - id: tlsDomainTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: transportMode === "faketls" - headerText: qsTr("FakeTLS domain") - textField.placeholderText: root.previousTlsDomain - Component.onCompleted: { - var savedDomain = tlsDomain - textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text - if (domainValue !== tlsDomain) { - tlsDomain = domainValue - TelemtConfigModel.setTlsDomain(tlsDomain) - } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - visible: transportMode === "faketls" - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") - color: AmneziaStyle.color.goldenApricot - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - } - - LabelWithButtonType { - id: advancedHeader - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 16 - property bool expanded: false - text: qsTr("Advanced") - rightImageSource: expanded - ? "qrc:/images/controls/chevron-up.svg" - : "qrc:/images/controls/chevron-down.svg" - rightImageColor: AmneziaStyle.color.mutedGray - clickedFunction: function () { - expanded = !expanded - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - visible: advancedHeader.expanded CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.topMargin: 8 - Layout.bottomMargin: 4 - text: qsTr("Additional secrets") + Layout.bottomMargin: 8 + visible: !fieldsEditable && !root.pageBusy + text: (containerStatus === 1 || containerStatus === 2) + ? qsTr("Enable Telemt to edit settings") + : (statusErrorCode !== 0 + ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) + : qsTr("Cannot reach the server — settings are unavailable")) color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap } - CaptionTextType { + + ColumnLayout { Layout.fillWidth: true + Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") - color: AmneziaStyle.color.charcoalGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } + Layout.bottomMargin: 16 * 2 + spacing: 4 + + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } - Repeater { - model: additionalSecrets - delegate: RowLayout { + RowLayout { Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 spacing: 8 + CaptionTextType { Layout.fillWidth: true - text: modelData - color: AmneziaStyle.color.paleGray - elide: Text.ElideMiddle - font.pixelSize: 13 - } - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.mutedGray - onClicked: { GC.copyToClipBoard(modelData) - PageController.showNotificationMessage(qsTr("Copied")) } + text: secret !== "" ? telemtActiveSecretForBaseHex(secret) : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 14 } + ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 + Layout.alignment: Qt.AlignTop + implicitWidth: 36 + implicitHeight: 36 hoverEnabled: true - image: "qrc:/images/controls/trash.svg" - imageColor: AmneziaStyle.color.vibrantRed + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable onClicked: { - TelemtConfigModel.removeAdditionalSecret(index) - root.telemtScheduleUpdate(false) + var secretSnapshot = secret + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + root.previousSecret = secretSnapshot + if (containerStatus === 1) { + isUpdating = true + TelemtConfigModel.generateSecret() + root.telemtScheduleUpdate(false) + } else { + TelemtConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) + } + }, + function () { + } + ) } } } } - BasicButtonType { + TextFieldWithHeaderType { + id: publicHostTextField + enabled: fieldsEditable Layout.fillWidth: true - Layout.topMargin: 8 - Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Add additional secret") - clickedFunc: function () { - TelemtConfigModel.addAdditionalSecret() - root.telemtScheduleUpdate(false) + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) + textField.text: publicHost + textField.maximumLength: 253 + textField.validator: PublicHostInputValidator { + } + textField.onTextChanged: { + var t = publicHostTextField.textField.text + if (TelemtConfigModel.isPublicHostTypingIncomplete(t)) { + publicHostTextField.errorText = "" + } else if (!TelemtConfigModel.isValidPublicHost(t)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + } else { + publicHostTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidPublicHost(textField.text)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + return + } + publicHostTextField.errorText = "" + if (textField.text !== publicHost) { + publicHost = textField.text + TelemtConfigModel.setPublicHost(publicHost) + } } } - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - LabelTextType { + CaptionTextType { Layout.fillWidth: true Layout.leftMargin: 16 + Layout.rightMargin: 16 Layout.bottomMargin: 4 - text: qsTr("Worker mode") + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap } - ButtonGroup { - id: workerModeGroup + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap } - RowLayout { + TextFieldWithHeaderType { + id: portTextField + enabled: fieldsEditable Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 4 - spacing: 0 - visible: transportMode !== "faketls" - - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Auto") - ButtonGroup.group: workerModeGroup - checked: workersMode === "auto" - onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: TelemtConfigModel.defaultPort() + textField.maximumLength: 5 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 1 + top: 65535 } - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Manual") - ButtonGroup.group: workerModeGroup - checked: workersMode === "manual" - onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort + } + textField.onTextChanged: { + var cur = portTextField.textField.text + var clean = TelemtConfigModel.sanitizePortFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + } } + textField.onEditingFinished: { + textField.text = TelemtConfigModel.sanitizePortFieldText(textField.text) + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap } CaptionTextType { @@ -1226,302 +1101,902 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 8 - visible: transportMode === "faketls" - text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") color: AmneziaStyle.color.mutedGray font.pixelSize: 12 wrapMode: Text.WordWrap } TextFieldWithHeaderType { - id: workersTextField + id: tagTextField + enabled: fieldsEditable Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 16 - visible: workersMode === "manual" && transportMode !== "faketls" - headerText: qsTr("Workers count") - textField.placeholderText: "2" - textField.text: workers - textField.maximumLength: 3 - textField.validator: IntValidator { - bottom: 1 - top: TelemtConfigModel.maxWorkers() + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") + textField.text: tag + textField.maximumLength: TelemtConfigModel.mtProxyBotTagHexLength() + textField.onTextChanged: { + var cur = tagTextField.textField.text + var clean = TelemtConfigModel.sanitizeMtProxyTagFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + return + } + var tt = tagTextField.textField.text + if (tt === "") { + tagTextField.errorText = "" + return + } + if (TelemtConfigModel.isMtProxyTagTypingIncomplete(tt)) { + tagTextField.errorText = "" + return + } + if (!TelemtConfigModel.isValidMtProxyTag(tt)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") + return + } + tagTextField.errorText = "" } textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== workers) { - workers = textField.text - TelemtConfigModel.setWorkers(workers) + var raw = textField.text.replace(/^\s+|\s+$/g, '') + var normalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(raw) + textField.text = normalized + if (!TelemtConfigModel.isValidMtProxyTag(normalized)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") + return + } + tagTextField.errorText = "" + if (normalized !== tag) { + tag = normalized + TelemtConfigModel.setTag(tag) } } } - DividerType { + RowLayout { Layout.fillWidth: true - Layout.bottomMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } } - SwitcherType { + CaptionTextType { Layout.fillWidth: true + Layout.leftMargin: 16 Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + text: qsTr("Transport mode") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + DropDownType { + id: transportModeDropDown + enabled: fieldsEditable + Layout.fillWidth: true Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Server is behind NAT / Docker bridge") - descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") - checked: natEnabled - onToggled: function () { - if (checked !== natEnabled) { - natEnabled = checked - TelemtConfigModel.setNatEnabled(natEnabled) + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.4 + headerText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + TelemtConfigModel.setTransportMode(transportMode) + root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 + transportModeDropDown.closeTriggered() + } + } } } } TextFieldWithHeaderType { - id: natInternalIpTextField + id: tlsDomainTextField + enabled: fieldsEditable Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("Internal IP") - textField.placeholderText: "172.17.0.2" - textField.text: natInternalIp + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + textField.validator: RegularExpressionValidator { + regularExpression: /^[A-Za-z0-9.-]*$/ + } + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onTextChanged: { + var t = tlsDomainTextField.textField.text + if (t === "" || TelemtConfigModel.isFakeTlsDomainTypingIncomplete(t) + || TelemtConfigModel.isValidFakeTlsDomain(t)) { + tlsDomainTextField.errorText = "" + } else { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + } + } textField.onEditingFinished: { textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== natInternalIp) { - natInternalIp = textField.text - TelemtConfigModel.setNatInternalIp(natInternalIp) + var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text + if (!TelemtConfigModel.isValidFakeTlsDomain(domainValue)) { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + return + } + tlsDomainTextField.errorText = "" + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + TelemtConfigModel.setTlsDomain(tlsDomain) } } } - TextFieldWithHeaderType { - id: natExternalIpTextField + ColumnLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("External IP") - textField.placeholderText: "1.2.3.4" - textField.text: natExternalIp - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (textField.text !== natExternalIp) { - natExternalIp = textField.text - TelemtConfigModel.setNatExternalIp(natExternalIp) - } + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 } } - } - - DividerType { - Layout.fillWidth: true - Layout.topMargin: 8 - } - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 8 - visible: containerStatus === 1 + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } - RowLayout { + ColumnLayout { Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + enabled: fieldsEditable - Header2Type { + CaptionTextType { Layout.fillWidth: true - headerText: qsTr("Diagnostics") + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: ColumnLayout { + id: addSecretDelegate + property bool linksExpanded: false + readonly property bool linksPanelAllowed: settingsRoot.telemtIsAdditionalPersisted(modelData) + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 0 + + onLinksPanelAllowedChanged: { + if (!linksPanelAllowed) { + linksExpanded = false + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: collapsedBar.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: collapsedBar + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 8 + + Item { + Layout.fillWidth: true + implicitHeight: Math.max(hexCaption.implicitHeight, 24) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + CaptionTextType { + id: hexCaption + Layout.fillWidth: true + text: settingsRoot.telemtActiveSecretForBaseHex(modelData) + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + + Image { + width: 24 + height: 24 + visible: addSecretDelegate.linksPanelAllowed + source: "qrc:/images/controls/chevron-down.svg" + sourceSize.width: 24 + sourceSize.height: 24 + rotation: addSecretDelegate.linksExpanded ? 180 : 0 + Behavior on rotation { + NumberAnimation { + duration: 150 + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: addSecretDelegate.linksPanelAllowed + enabled: addSecretDelegate.linksPanelAllowed + cursorShape: Qt.PointingHandCursor + onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded + } + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: settingsRoot.telemtRemoveAdditionalSecret(index) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 8 + spacing: 8 + visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTmeRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTmeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.telemtTmeLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTmeLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTmeLinkForAdditional(modelData)) + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTgRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.telemtTgLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTgLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTgLinkForAdditional(modelData)) + } + } + } + } + } } - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray - hoverEnabled: !diagLoading - enabled: !diagLoading - onClicked: { - diagLoading = true - InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + TelemtConfigModel.addAdditionalSecret() } } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 } - CaptionTextType { + + LabelTextType { Layout.fillWidth: true - text: qsTr("Public port reachable") - color: AmneziaStyle.color.paleGray + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + + ButtonGroup { + id: workerModeGroup } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } + } } + CaptionTextType { Layout.fillWidth: true - text: qsTr("Telegram upstream reachable") - color: AmneziaStyle.color.paleGray + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 2 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 0 + top: TelemtConfigModel.maxWorkers() + } + textField.onTextChanged: { + var cur = workersTextField.textField.text + if (cur === "") { + return + } + var n = parseInt(cur, 10) + var maxW = TelemtConfigModel.maxWorkers() + if (isNaN(n) || n < 0) { + n = 0 + } + if (n > maxW) { + n = maxW + } + var clamped = String(n) + if (clamped !== cur) { + textField.text = clamped + textField.cursorPosition = clamped.length + } + } + textField.onEditingFinished: { + var v = workersTextField.textField.text + if (v !== "") { + var m = parseInt(v, 10) + var maxW2 = TelemtConfigModel.maxWorkers() + if (isNaN(m) || m < 0) { + m = 0 + } + if (m > maxW2) { + m = maxW2 + } + v = String(m) + textField.text = v + } + if (v !== workers) { + workers = v + TelemtConfigModel.setWorkers(workers) + } + } } - } - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 } - CaptionTextType { + + SwitcherType { Layout.fillWidth: true - text: qsTr("Clients connected") - color: AmneziaStyle.color.paleGray + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + TelemtConfigModel.setNatEnabled(natEnabled) + } + } } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() - color: AmneziaStyle.color.paleGray + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natInternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natInternalIpTextField.errorText = "" + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + TelemtConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natExternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natExternalIpTextField.errorText = "" + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + TelemtConfigModel.setNatExternalIp(natExternalIp) + } + } } } - RowLayout { + DividerType { Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: AmneziaStyle.color.mutedGray + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + } + } } - CaptionTextType { + + RowLayout { Layout.fillWidth: true - text: qsTr("Last config refresh") - color: AmneziaStyle.color.paleGray + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } } - CaptionTextType { - text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") - color: AmneziaStyle.color.mutedGray + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } } - } - LabelWithButtonType { - Layout.fillWidth: true - Layout.leftMargin: -16 - visible: diagStatsEndpoint !== "" - text: qsTr("Stats endpoint") - descriptionText: diagStatsEndpoint - descriptionOnTop: true - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray - clickedFunction: function () { - GC.copyToClipBoard(diagStatsEndpoint) - PageController.showNotificationMessage(qsTr("Copied")) + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 } } CaptionTextType { Layout.fillWidth: true - text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") color: AmneziaStyle.color.mutedGray - visible: diagClientsConnected < 0 + wrapMode: Text.WordWrap + font.pixelSize: 12 } - } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - Layout.bottomMargin: 24 - text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 32 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - text: qsTr("Save") - clickedFunc: function () { - var portValue = portTextField.textField.text === "" - ? TelemtConfigModel.defaultPort() - : portTextField.textField.text - if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { - portTextField.errorText = qsTr("The port must be in the range of 1 to 65535") - return - } - TelemtConfigModel.setPort(portValue) - TelemtConfigModel.setTag(tagTextField.textField.text) - TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) - TelemtConfigModel.setTransportMode(transportMode) - var domainValue = tlsDomainTextField.textField.text === "" - ? TelemtConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - TelemtConfigModel.setTlsDomain(domainValue) - - if (transportMode === "faketls") { - workers = "0" - TelemtConfigModel.setWorkers("0") - } else { - TelemtConfigModel.setWorkersMode(workersMode) - TelemtConfigModel.setWorkers(workers) + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable && !root.telemtNetworkBlocked + text: qsTr("Save") + clickedFunc: function () { + if (root.telemtNetworkBlocked) { + PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change Telemt settings.")) + return + } + publicHostTextField.errorText = "" + tagTextField.errorText = "" + tlsDomainTextField.errorText = "" + natInternalIpTextField.errorText = "" + natExternalIpTextField.errorText = "" + portTextField.errorText = "" + + var portValue = portTextField.textField.text === "" + ? TelemtConfigModel.defaultPort() + : portTextField.textField.text + + var errorLines = [] + var bullet = "- " + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + var portErr = qsTr("The port must be in the range of 1 to 65535") + portTextField.errorText = portErr + errorLines.push(bullet + portErr) + } + if (!TelemtConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { + var hostErr = qsTr("Enter a valid IP address or domain name") + publicHostTextField.errorText = hostErr + errorLines.push(bullet + hostErr) + } + var tagNormalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) + tagTextField.textField.text = tagNormalized + if (!TelemtConfigModel.isValidMtProxyTag(tagNormalized)) { + var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") + tagTextField.errorText = tagErr + errorLines.push(bullet + tagErr) + } + var domainValueForSave = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + if (!TelemtConfigModel.isValidFakeTlsDomain(domainValueForSave)) { + var tlsErr = qsTr("Enter a valid domain name") + tlsDomainTextField.errorText = tlsErr + errorLines.push(bullet + tlsErr) + } + var natIpErr = qsTr("Enter a valid IPv4 address") + if (!TelemtConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { + natInternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) + } + if (!TelemtConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { + natExternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) + } + if (errorLines.length > 0) { + PageController.showErrorMessage(errorLines.join("\n")) + return + } + TelemtConfigModel.setPort(portValue) + TelemtConfigModel.setTag(tagNormalized) + TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) + TelemtConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + TelemtConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + TelemtConfigModel.setWorkers("0") + } else { + TelemtConfigModel.setWorkersMode(workersMode) + TelemtConfigModel.setWorkers(workers) + } + TelemtConfigModel.setNatEnabled(natEnabled) + TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + root.previousSecret = secret + isUpdating = true + root.telemtScheduleUpdate(false) } - TelemtConfigModel.setNatEnabled(natEnabled) - TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) - TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) - - previousPort = port - previousTag = tag - previousPublicHost = publicHost - previousTransportMode = transportMode - previousTlsDomain = tlsDomain - previousWorkersMode = workersMode - previousWorkers = workers - previousNatEnabled = natEnabled - previousNatInternalIp = natInternalIp - previousNatExternalIp = natExternalIp - root.previousSecret = secret - isUpdating = true - root.telemtScheduleUpdate(false) } } } } } - - } } diff --git a/client/ui/qml/Pages2/PageShareConnection.qml b/client/ui/qml/Pages2/PageShareConnection.qml index 20716e83b7..3a50ea67cb 100644 --- a/client/ui/qml/Pages2/PageShareConnection.qml +++ b/client/ui/qml/Pages2/PageShareConnection.qml @@ -320,7 +320,7 @@ PageType { Layout.rightMargin: 16 visible: isQrCodeVisible horizontalAlignment: Text.AlignHCenter - text: qsTr("To read the QR code in the Amnezia app, select \"Add server\" → \"I have data to connect\" → \"QR code, key or settings file\"") + text: qsTr("To read the QR code in the Amnezia app, tap + in the main menu → 'QR code'") } } }