fix Public host|Port|Promoted tag|FakeTLS domain|Internal/External IP

This commit is contained in:
dranik
2026-06-15 19:03:15 +03:00
parent b5c047565c
commit 0d99459670
4 changed files with 497 additions and 13 deletions
+322 -7
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;
@@ -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;