Compare commits

...

7 Commits

Author SHA1 Message Date
dranik
07aad87874 fixed timeout & add message & fix disabled telemt|mtproxy 2026-06-16 13:58:22 +03:00
dranik
6202078e0c fixed SshSession parent 2026-06-16 11:14:33 +03:00
dranik
4cc07acae5 fixed diagnostic mtproxy/telemt 2026-06-15 21:01:11 +03:00
dranik
0d99459670 fix Public host|Port|Promoted tag|FakeTLS domain|Internal/External IP 2026-06-15 19:03:15 +03:00
dranik
b5c047565c fix FakeTLS domain input & port space & workers 2026-06-15 18:23:55 +03:00
dranik
60904b9fde fixed UI 'Transport mode' 'Share QR Code' 2026-06-15 17:32:17 +03:00
yp
cc404378f9 fix: remove only amnezia- prefixed docker volumes (#2728) 2026-06-15 13:12:19 +07:00
16 changed files with 829 additions and 112 deletions

View File

@@ -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;
@@ -835,6 +835,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 +984,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 +1012,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 +1034,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 +1143,7 @@ ErrorCode InstallController::scanServerForInstalledContainers(const QString &ser
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
@@ -1172,7 +1186,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<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
if (errorCode) {
@@ -1241,7 +1255,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
if (!credentials.isValid()) {
return ErrorCode::InternalError;
}
SshSession sshSession(this);
SshSession sshSession;
QMap<DockerContainer, ContainerConfig> installedContainers;
ErrorCode errorCode = getAlreadyInstalledContainers(credentials, installedContainers, sshSession);
@@ -1283,7 +1297,7 @@ ErrorCode InstallController::installContainer(const QString &serverId, DockerCon
ErrorCode InstallController::checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback)
{
SshSession sshSession(this);
SshSession sshSession;
ErrorCode errorCode = ErrorCode::NoError;
if (credentials.secretData.contains("BEGIN") && credentials.secretData.contains("PRIVATE KEY")) {
@@ -1564,7 +1578,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 +1618,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 +1652,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 +1675,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);

View File

@@ -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);
out = { };
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
const QString containerName = ContainerUtils::containerToString(container);
const bool isTelemt = container == DockerContainer::Telemt;
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();
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,

View File

@@ -271,6 +271,7 @@ namespace amnezia
constexpr char workersModeAuto[] = "auto";
constexpr char workersModeManual[] = "manual";
constexpr int maxWorkers = 32;
constexpr int botTagHexLength = 32;
}
} // namespace protocols

View File

@@ -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");

View File

@@ -21,6 +21,7 @@ enum SharedScriptType {
// General scripts
prepare_host,
install_docker,
install_conntrack,
build_container,
remove_container,
remove_all_containers,

View File

@@ -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"

View File

@@ -1,6 +1,6 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a --format table | grep amnezia | awk '{print $3, $1 ":" $2}' | xargs sudo docker rmi;\
sudo docker volume ls | grep amnezia | awk '{print $2}' | xargs sudo docker volume rm -f;\
sudo docker volume ls --format '{{.Name}}' | grep '^amnezia-' | xargs -r sudo docker volume rm -f;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia

View File

@@ -18,6 +18,7 @@
<file>dns/Dockerfile</file>
<file>dns/run_container.sh</file>
<file>install_docker.sh</file>
<file>install_conntrack.sh</file>
<file>ipsec/configure_container.sh</file>
<file>ipsec/Dockerfile</file>
<file>ipsec/mobileconfig.plist</file>

View File

@@ -9,6 +9,7 @@
#include <QStandardPaths>
#include <QFutureWatcher>
#include <QtConcurrent>
#include <utility>
#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;
}
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<ErrorCode>(this);
QObject::connect(watcher, &QFutureWatcher<ErrorCode>::finished, this,
[this, watcher, serverId, container, enabled]() {
const ErrorCode errorCode = watcher->result();
watcher->deleteLater();
emit serverIsBusy(false);
emit installationErrorOccurred(errorCode);
if (errorCode == ErrorCode::NoError) {
const ContainerConfig currentConfig = m_serversController->getContainerConfig(serverId, container);
m_protocolModel->updateModel(currentConfig);
emit setContainerEnabledFinished(enabled);
return;
}
emit installationErrorOccurred(errorCode);
});
QFuture<ErrorCode> 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<int, int>; // {status, errorCode}
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<StatusResult>(this);
QObject::connect(watcher, &QFutureWatcher<StatusResult>::finished, this, [this, watcher]() {
const StatusResult result = watcher->result();
watcher->deleteLater();
emit containerStatusRefreshed(result.first, result.second);
});
QFuture<StatusResult> 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<int>(errorCode) };
}
return { status, static_cast<int>(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<bool, MtProxyContainerDiagnostics>;
InstallController *installController = m_installController;
auto *watcher = new QFutureWatcher<DiagResult>(this);
QObject::connect(watcher, &QFutureWatcher<DiagResult>::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<DiagResult> 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<QString>(this);
QObject::connect(watcher, &QFutureWatcher<QString>::finished, this, [this, watcher]() {
const QString secret = watcher->result();
watcher->deleteLater();
emit containerSecretFetched(secret);
});
QFuture<QString> future = QtConcurrent::run([installController, serverId, container]() -> QString {
return installController->fetchDockerContainerSecret(serverId, container);
});
watcher->setFuture(future);
}
void InstallUiController::rebootServer(const QString &serverId)

View File

@@ -114,7 +114,7 @@ signals:
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);

View File

@@ -398,6 +398,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+$)"));

View File

@@ -1,7 +1,13 @@
#include "telemtConfigModel.h"
#include <QRegularExpression>
#include "ui/models/utils/mtproxy_public_host_input.h"
#include <QHostAddress>
#include <QRegExp>
#include <QRegularExpression>
#include <qqml.h>
#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<PublicHostInputValidator>("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: {
@@ -379,6 +407,293 @@ QString TelemtConfigModel::workersModeManual() const {
return QString::fromUtf8(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) {
// Reject unusable special addresses such as "::" (any), loopback and null.
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;
}
// 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 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();
}
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
const QRegularExpressionMatch m = runHex.match(trimmed);
if (m.hasMatch()) {
return m.captured(1);
}
const int cap = protocols::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<int, QByteArray> TelemtConfigModel::roleNames() const {
QHash<int, QByteArray> roles;

View File

@@ -116,12 +116,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<int, QByteArray> 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;

View File

@@ -21,6 +21,8 @@ PageType {
id: root
property int containerStatus: 1
// Last status-query error code (0 = none). 305 = SshTimeoutError → server unreachable.
property int statusErrorCode: 0
property bool isUpdating: false
property bool isCheckingStatus: false
property bool isFetchingSecret: false
@@ -261,6 +263,7 @@ PageType {
isCheckingStatus = false
isFetchingSecret = false
busyIndicatorShown = false
statusErrorCode = 0
PageController.disableControls(false)
PageController.showBusyIndicator(false)
diagLoading = false
@@ -348,13 +351,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()
@@ -842,6 +850,8 @@ PageType {
width: settingsListView.width
spacing: 0
readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy
function mtProxyActiveSecretForBaseHex(baseHex) {
return root.mtProxyClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex,
root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain())
@@ -887,6 +897,21 @@ PageType {
}
}
CaptionTextType {
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
wrapMode: Text.WordWrap
}
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 16
@@ -921,6 +946,7 @@ PageType {
image: "qrc:/images/controls/refresh-cw.svg"
imageColor: AmneziaStyle.color.paleGray
visible: ServersUiController.isProcessedServerHasWriteAccess()
enabled: fieldsEditable
onClicked: {
var secretSnapshot = secret
showQuestionDrawer(
@@ -949,6 +975,7 @@ PageType {
TextFieldWithHeaderType {
id: publicHostTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1010,6 +1037,7 @@ PageType {
TextFieldWithHeaderType {
id: portTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1017,6 +1045,7 @@ PageType {
headerText: qsTr("Server port")
textField.placeholderText: MtProxyConfigModel.defaultPort()
textField.maximumLength: 5
textField.inputMethodHints: Qt.ImhDigitsOnly
textField.validator: IntValidator {
bottom: 1
top: 65535
@@ -1025,8 +1054,16 @@ PageType {
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
}
}
textField.onEditingFinished: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
textField.text = MtProxyConfigModel.sanitizePortFieldText(textField.text)
}
}
@@ -1055,6 +1092,7 @@ PageType {
TextFieldWithHeaderType {
id: tagTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1138,6 +1176,7 @@ PageType {
DropDownType {
id: transportModeDropDown
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1173,6 +1212,7 @@ PageType {
TextFieldWithHeaderType {
id: tlsDomainTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1180,10 +1220,22 @@ PageType {
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
@@ -1243,6 +1295,7 @@ PageType {
Layout.fillWidth: true
spacing: 0
visible: advancedHeader.expanded
enabled: fieldsEditable
CaptionTextType {
Layout.fillWidth: true
@@ -1560,15 +1613,41 @@ PageType {
headerText: qsTr("Workers count")
textField.placeholderText: "2"
textField.text: workers
textField.maximumLength: 3
textField.maximumLength: 2
textField.inputMethodHints: Qt.ImhDigitsOnly
// Range input like the port field: IntValidator bounds the value and the
// clamp keeps it within 0..maxWorkers on every change (rejects 33+, neg.).
textField.validator: IntValidator {
bottom: 1
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: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (textField.text !== workers) {
workers = textField.text
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)
}
}
@@ -1824,7 +1903,7 @@ PageType {
Layout.rightMargin: 16
Layout.leftMargin: 16
visible: ServersUiController.isProcessedServerHasWriteAccess()
enabled: !root.mtProxyNetworkBlocked
enabled: fieldsEditable && !root.mtProxyNetworkBlocked
text: qsTr("Save")
clickedFunc: function () {
if (root.mtProxyNetworkBlocked) {

View File

@@ -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
@@ -41,6 +43,35 @@ PageType {
property string savedTlsDomain: ""
property string savedPublicHost: ""
readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/
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
}
onSavedTransportModeChanged: {
if (savedTransportMode === "faketls") {
root.syncedSecretTabIndex = 1
@@ -205,6 +236,7 @@ PageType {
isCheckingStatus = false
isFetchingSecret = false
busyIndicatorShown = false
statusErrorCode = 0
PageController.disableControls(false)
PageController.showBusyIndicator(false)
diagLoading = false
@@ -294,13 +326,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()
@@ -787,6 +824,8 @@ PageType {
width: settingsListView.width
spacing: 0
readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy
function telemtActiveSecretForBaseHex(baseHex) {
return root.telemtClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex,
root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain())
@@ -820,6 +859,21 @@ PageType {
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
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
}
ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 16
@@ -854,6 +908,7 @@ PageType {
image: "qrc:/images/controls/refresh-cw.svg"
imageColor: AmneziaStyle.color.paleGray
visible: ServersUiController.isProcessedServerHasWriteAccess()
enabled: fieldsEditable
onClicked: {
var secretSnapshot = secret
showQuestionDrawer(
@@ -882,6 +937,7 @@ PageType {
TextFieldWithHeaderType {
id: publicHostTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -889,8 +945,26 @@ PageType {
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)
@@ -925,6 +999,7 @@ PageType {
TextFieldWithHeaderType {
id: portTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -932,6 +1007,7 @@ PageType {
headerText: qsTr("Server port")
textField.placeholderText: TelemtConfigModel.defaultPort()
textField.maximumLength: 5
textField.inputMethodHints: Qt.ImhDigitsOnly
textField.validator: IntValidator {
bottom: 1
top: 65535
@@ -940,8 +1016,16 @@ PageType {
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 = textField.text.replace(/^\s+|\s+$/g, '')
textField.text = TelemtConfigModel.sanitizePortFieldText(textField.text)
var portValue = textField.text === "" ? TelemtConfigModel.defaultPort() : textField.text
if (portValue !== port) {
port = portValue
@@ -964,18 +1048,49 @@ PageType {
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("leave empty if not needed")
textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)")
textField.text: tag
textField.maximumLength: 64
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 !== tag) {
tag = textField.text
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)
}
}
@@ -1005,17 +1120,27 @@ PageType {
}
}
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.topMargin: 16 * 2
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
drawerParent: root
drawerHeight: 0.35
descriptionText: qsTr("Transport mode")
headerText: qsTr("Transport mode")
text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto")
listView: Component {
@@ -1043,6 +1168,7 @@ PageType {
TextFieldWithHeaderType {
id: tlsDomainTextField
enabled: fieldsEditable
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -1050,13 +1176,30 @@ PageType {
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, '')
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)
@@ -1108,6 +1251,7 @@ PageType {
Layout.fillWidth: true
spacing: 0
visible: advancedHeader.expanded
enabled: fieldsEditable
CaptionTextType {
Layout.fillWidth: true
@@ -1243,15 +1387,41 @@ PageType {
headerText: qsTr("Workers count")
textField.placeholderText: "2"
textField.text: workers
textField.maximumLength: 3
textField.maximumLength: 2
textField.inputMethodHints: Qt.ImhDigitsOnly
// Range input like the port field: IntValidator bounds the value and the
// clamp keeps it within 0..maxWorkers on every change (rejects 33+, neg.).
textField.validator: IntValidator {
bottom: 1
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: {
textField.text = textField.text.replace(/^\s+|\s+$/g, '')
if (textField.text !== workers) {
workers = textField.text
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)
}
}
@@ -1288,8 +1458,24 @@ PageType {
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)
@@ -1307,8 +1493,24 @@ PageType {
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)
@@ -1475,6 +1677,7 @@ PageType {
Layout.rightMargin: 16
Layout.leftMargin: 16
visible: ServersUiController.isProcessedServerHasWriteAccess()
enabled: fieldsEditable
text: qsTr("Save")
clickedFunc: function () {
var portValue = portTextField.textField.text === ""

View File

@@ -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'")
}
}
}