mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
Feat: Add MtProxy (Telegram)
This commit is contained in:
@@ -74,6 +74,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return container == DockerContainer::Sftp;
|
||||
case IsTorWebsiteRole: return container == DockerContainer::TorWebSite;
|
||||
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
|
||||
case IsMtProxyRole: return container == DockerContainer::MtProxy;
|
||||
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
|
||||
}
|
||||
|
||||
@@ -184,5 +185,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsTorWebsiteRole] = "isTorWebsite";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ public:
|
||||
IsDnsRole,
|
||||
IsSftpRole,
|
||||
IsTorWebsiteRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
};
|
||||
|
||||
Q_INVOKABLE void openContainerSettings(int containerIndex);
|
||||
|
||||
@@ -42,6 +42,7 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
|
||||
roles[IsSftpRole] = "isSftp";
|
||||
roles[IsIpsecRole] = "isIpsec";
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
|
||||
return roles;
|
||||
}
|
||||
@@ -71,6 +72,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
|
||||
case IsSftpRole: return proto == Proto::Sftp;
|
||||
case IsIpsecRole: return proto == Proto::Ikev2;
|
||||
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
|
||||
case IsMtProxyRole: return proto == Proto::MtProxy;
|
||||
case RawConfigRole:
|
||||
return getRawConfig();
|
||||
case IsClientProtocolExistsRole:
|
||||
@@ -124,6 +126,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
|
||||
case Proto::Dns: return PageLoader::PageEnum::PageServiceDnsSettings;
|
||||
case Proto::Sftp: return PageLoader::PageEnum::PageServiceSftpSettings;
|
||||
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
|
||||
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
|
||||
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ public:
|
||||
IsXrayRole,
|
||||
IsSftpRole,
|
||||
IsIpsecRole,
|
||||
IsSocks5ProxyRole
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
};
|
||||
|
||||
explicit ProtocolsModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
#include "mtProxyConfigModel.h"
|
||||
|
||||
#include "ui/models/utils/mtproxy_public_host_input.h"
|
||||
|
||||
#include "core/utils/networkUtilities.h"
|
||||
#include "core/utils/qrCodeUtils.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "qrcodegen.hpp"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QHostAddress>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <QtGlobal>
|
||||
#include <qqml.h>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
MtProxyConfigModel::MtProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {
|
||||
qmlRegisterType<PublicHostInputValidator>("MtProxyConfig", 1, 0, "PublicHostInputValidator");
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::rowCount(const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
m_protocolConfig.port = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
const QString tag = sanitizeMtProxyTagFieldText(value.toString());
|
||||
if (!isValidMtProxyTag(tag)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tag = tag;
|
||||
break;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
const QString h = value.toString().trimmed();
|
||||
if (!isValidPublicHost(h)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.publicHost = h;
|
||||
break;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
m_protocolConfig.transportMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
const QString d = value.toString().trimmed();
|
||||
if (!isValidFakeTlsDomain(d)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.tlsDomain = d;
|
||||
break;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
m_protocolConfig.additionalSecrets = value.toStringList();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
m_protocolConfig.workersMode = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
m_protocolConfig.workers = value.toString();
|
||||
break;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
m_protocolConfig.natEnabled = value.toBool();
|
||||
break;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natInternalIp = ip;
|
||||
break;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
const QString ip = value.toString().trimmed();
|
||||
if (!isValidOptionalIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.natExternalIp = ip;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant MtProxyConfigModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() != 0) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case Roles::PortRole: {
|
||||
return m_protocolConfig.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : m_protocolConfig.port;
|
||||
}
|
||||
case Roles::SecretRole: {
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
case Roles::TagRole: {
|
||||
return m_protocolConfig.tag;
|
||||
}
|
||||
case Roles::TgLinkRole: {
|
||||
return m_protocolConfig.tgLink;
|
||||
}
|
||||
case Roles::TmeLinkRole: {
|
||||
return m_protocolConfig.tmeLink;
|
||||
}
|
||||
case Roles::IsEnabledRole: {
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
case Roles::PublicHostRole: {
|
||||
return m_protocolConfig.publicHost.isEmpty()
|
||||
? m_fullConfig.value(configKey::hostName).toString()
|
||||
: m_protocolConfig.publicHost;
|
||||
}
|
||||
case Roles::TransportModeRole: {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
case Roles::TlsDomainRole: {
|
||||
return m_protocolConfig.tlsDomain;
|
||||
}
|
||||
case Roles::AdditionalSecretsRole: {
|
||||
return m_protocolConfig.additionalSecrets;
|
||||
}
|
||||
case Roles::WorkersModeRole: {
|
||||
return m_protocolConfig.workersMode.isEmpty()
|
||||
? QString(protocols::mtProxy::workersModeAuto)
|
||||
: m_protocolConfig.workersMode;
|
||||
}
|
||||
case Roles::WorkersRole: {
|
||||
return m_protocolConfig.workers.isEmpty() ? QString(protocols::mtProxy::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
}
|
||||
case Roles::NatEnabledRole: {
|
||||
return m_protocolConfig.natEnabled;
|
||||
}
|
||||
case Roles::NatInternalIpRole: {
|
||||
return m_protocolConfig.natInternalIp;
|
||||
}
|
||||
case Roles::NatExternalIpRole: {
|
||||
return m_protocolConfig.natExternalIp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(amnezia::DockerContainer container,
|
||||
const amnezia::MtProxyProtocolConfig &protocolConfig) {
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::updateModel(const QJsonObject &config) {
|
||||
beginResetModel();
|
||||
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = MtProxyProtocolConfig::fromJson(config.value(configKey::mtproxy).toObject());
|
||||
if (m_protocolConfig.port.isEmpty()) m_protocolConfig.port = protocols::mtProxy::defaultPort;
|
||||
if (m_protocolConfig.transportMode.isEmpty()) m_protocolConfig.transportMode = protocols::mtProxy::transportModeStandard;
|
||||
if (m_protocolConfig.workersMode.isEmpty()) m_protocolConfig.workersMode = protocols::mtProxy::workersModeAuto;
|
||||
if (m_protocolConfig.workers.isEmpty()) m_protocolConfig.workers = protocols::mtProxy::defaultWorkers;
|
||||
{
|
||||
QString tagIn = sanitizeMtProxyTagFieldText(m_protocolConfig.tag);
|
||||
if (!isValidMtProxyTag(tagIn)) {
|
||||
tagIn.clear();
|
||||
}
|
||||
m_protocolConfig.tag = tagIn;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject MtProxyConfigModel::getConfig() {
|
||||
m_fullConfig.insert(configKey::mtproxy, m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::generateSecret() {
|
||||
// Generate 16 random bytes = 32 hex chars
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setSecret(const QString &secret) {
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::validateAndSetSecret(const QString &rawSecret) {
|
||||
if (!QRegularExpression("^[0-9a-fA-F]{32}$").match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
setData(index(0), rawSecret, SecretRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPort(const QString &port) {
|
||||
setData(index(0), port, PortRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTag(const QString &tag) {
|
||||
setData(index(0), tag, TagRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setPublicHost(const QString &host) {
|
||||
const QString t = host.trimmed();
|
||||
if (!isValidPublicHost(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, PublicHostRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTransportMode(const QString &mode) {
|
||||
setData(index(0), mode, TransportModeRole);
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTransportMode() const {
|
||||
return m_protocolConfig.transportMode.isEmpty()
|
||||
? QString(protocols::mtProxy::transportModeStandard)
|
||||
: m_protocolConfig.transportMode;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getTlsDomain() const {
|
||||
return m_protocolConfig.tlsDomain.isEmpty()
|
||||
? QString(protocols::mtProxy::defaultTlsDomain)
|
||||
: m_protocolConfig.tlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::getPublicHost() const {
|
||||
return m_protocolConfig.publicHost;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setTlsDomain(const QString &domain) {
|
||||
const QString t = domain.trimmed();
|
||||
if (!isValidFakeTlsDomain(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, TlsDomainRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkersMode(const QString &mode) {
|
||||
setData(index(0), mode, WorkersModeRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setWorkers(const QString &workers) {
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatEnabled(bool enabled) {
|
||||
setData(index(0), enabled, NatEnabledRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatInternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatInternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setNatExternalIp(const QString &ip) {
|
||||
const QString t = ip.trimmed();
|
||||
if (!isValidOptionalIpv4(t)) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), t, NatExternalIpRole);
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::addAdditionalSecret() {
|
||||
QString newSecret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
newSecret += QString("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
m_protocolConfig.additionalSecrets.append(newSecret);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::removeAdditionalSecret(int idx) {
|
||||
if (idx < 0 || idx >= m_protocolConfig.additionalSecrets.size()) {
|
||||
return;
|
||||
}
|
||||
m_protocolConfig.additionalSecrets.removeAt(idx);
|
||||
emit dataChanged(index(0), index(0), QList<int>{AdditionalSecretsRole});
|
||||
}
|
||||
|
||||
QVariantList MtProxyConfigModel::additionalSecretsList() const {
|
||||
QVariantList out;
|
||||
out.reserve(m_protocolConfig.additionalSecrets.size());
|
||||
for (const auto &s: m_protocolConfig.additionalSecrets) {
|
||||
if (!s.isEmpty()) {
|
||||
out.append(s);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void MtProxyConfigModel::setEnabled(bool enabled) {
|
||||
m_protocolConfig.isEnabled = enabled;
|
||||
emit dataChanged(index(0), index(0), QList<int>{IsEnabledRole});
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::generateQrCode(const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
auto qr = qrCodeUtils::generateQrCode(text.toUtf8());
|
||||
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultTlsDomain() const {
|
||||
return protocols::mtProxy::defaultTlsDomain;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultPort() const {
|
||||
return protocols::mtProxy::defaultPort;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::defaultWorkers() const {
|
||||
return protocols::mtProxy::defaultWorkers;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::maxWorkers() const {
|
||||
return protocols::mtProxy::maxWorkers;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeStandard() const {
|
||||
return protocols::mtProxy::transportModeStandard;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::transportModeFakeTLS() const {
|
||||
return protocols::mtProxy::transportModeFakeTLS;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeAuto() const {
|
||||
return protocols::mtProxy::workersModeAuto;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::workersModeManual() const {
|
||||
return protocols::mtProxy::workersModeManual;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidPublicHost(const QString &host) const {
|
||||
const QString t = host.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv4Protocol) {
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return NetworkUtilities::domainRegExp().exactMatch(t);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostInputAllowed(const QString &text) const {
|
||||
return mtproxyPublicHostInputAllowed(text);
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isPublicHostTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (isValidPublicHost(t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (onlyDigitDot.match(t).hasMatch()) {
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
const QStringList parts = t.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() < 4) {
|
||||
return true;
|
||||
}
|
||||
for (const QString &part: parts) {
|
||||
if (part.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t.contains(QLatin1Char(':'))) {
|
||||
if (t.contains(QLatin1String(":::"))) {
|
||||
return false;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char(':'))) {
|
||||
return true;
|
||||
}
|
||||
QHostAddress a(t);
|
||||
if (a.protocol() == QHostAddress::IPv6Protocol) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1String("::")) && t.count(QLatin1Char(':')) < 7 && !t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidMtProxyTag(const QString &tag) const {
|
||||
if (tag.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral("^([0-9a-fA-F]{%1})$").arg(protocols::mtProxy::botTagHexLength));
|
||||
return re.match(tag).hasMatch();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isMtProxyTagTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression hexOnly(QStringLiteral(R"(^[0-9a-fA-F]*$)"));
|
||||
if (!hexOnly.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return t.size() < protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
int MtProxyConfigModel::mtProxyBotTagHexLength() const {
|
||||
return protocols::mtProxy::botTagHexLength;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidFakeTlsDomain(const QString &domain) const {
|
||||
const QString t = domain.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
QHostAddress addr;
|
||||
if (addr.setAddress(t)) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyAsciiDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyAsciiDigits.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
QRegExp re(NetworkUtilities::domainRegExp());
|
||||
re.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
if (!re.exactMatch(t)) {
|
||||
return false;
|
||||
}
|
||||
// ee + 32 hex (base secret) + hex(UTF-8 domain); keep headroom under typical client limits.
|
||||
if (t.toUtf8().size() > 111) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::clipboardText() const {
|
||||
if (QClipboard *c = QGuiApplication::clipboard()) {
|
||||
return c->text();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeFakeTlsDomainFieldText(const QString &input) const {
|
||||
const QString t = normalizeFakeTlsDomainInput(input);
|
||||
QString out;
|
||||
out.reserve(t.size());
|
||||
for (const QChar &c: t) {
|
||||
const ushort u = c.unicode();
|
||||
const bool letter = (u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z');
|
||||
const bool digit = (u >= '0' && u <= '9');
|
||||
if (letter || digit || u == '.' || u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
if (out.size() > 253) {
|
||||
out.truncate(253);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainInputAllowed(const QString &text) const {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression re(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
return re.match(text).hasMatch();
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePublicHostFieldText(const QString &input) const {
|
||||
QString out;
|
||||
const int cap = qMin(input.size(), 253);
|
||||
out.reserve(cap);
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 253) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '.' || u == ':' ||
|
||||
u == '-') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizePortFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 5));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 5) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeMtProxyTagFieldText(const QString &input) const {
|
||||
QString trimmed = input.trimmed();
|
||||
if (trimmed.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
trimmed = trimmed.mid(2).trimmed();
|
||||
}
|
||||
// Prefer a contiguous 32-hex run (paste from bot message with extra text).
|
||||
static const QRegularExpression runHex(QStringLiteral(R"(([0-9a-fA-F]{32}))"));
|
||||
const QRegularExpressionMatch m = runHex.match(trimmed);
|
||||
if (m.hasMatch()) {
|
||||
return m.captured(1);
|
||||
}
|
||||
const int cap = protocols::mtProxy::botTagHexLength;
|
||||
QString out;
|
||||
out.reserve(qMin(trimmed.size(), cap));
|
||||
for (const QChar &c: trimmed) {
|
||||
if (out.size() >= cap) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || (u >= 'a' && u <= 'f') || (u >= 'A' && u <= 'F')) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 3));
|
||||
for (const QChar &c: input) {
|
||||
const ushort u = c.unicode();
|
||||
if (u >= '0' && u <= '9' && out.size() < 3) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::sanitizeOptionalIpv4FieldText(const QString &input) const {
|
||||
QString out;
|
||||
out.reserve(qMin(input.size(), 15));
|
||||
for (const QChar &c: input) {
|
||||
if (out.size() >= 15) {
|
||||
break;
|
||||
}
|
||||
const ushort u = c.unicode();
|
||||
if ((u >= '0' && u <= '9') || u == '.') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString MtProxyConfigModel::normalizeFakeTlsDomainInput(const QString &input) const {
|
||||
QString t = input.trimmed();
|
||||
if (t.startsWith(QLatin1String("https://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(8);
|
||||
} else if (t.startsWith(QLatin1String("http://"), Qt::CaseInsensitive)) {
|
||||
t = t.mid(7);
|
||||
}
|
||||
if (const int slash = t.indexOf(QLatin1Char('/')); slash >= 0) {
|
||||
t = t.left(slash);
|
||||
}
|
||||
if (const int at = t.indexOf(QLatin1Char('@')); at >= 0) {
|
||||
t = t.mid(at + 1);
|
||||
}
|
||||
if (const int colon = t.indexOf(QLatin1Char(':')); colon >= 0) {
|
||||
t = t.left(colon);
|
||||
}
|
||||
if (t.startsWith(QLatin1String("www."), Qt::CaseInsensitive)) {
|
||||
const QString rest = t.mid(4);
|
||||
if (rest.contains(QLatin1Char('.'))) {
|
||||
t = rest;
|
||||
}
|
||||
}
|
||||
return t.trimmed();
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isFakeTlsDomainTypingIncomplete(const QString &text) const {
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (isValidFakeTlsDomain(t)) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1Char('/')) || t.contains(QLatin1Char(':')) || t.contains(QLatin1Char('@'))
|
||||
|| t.contains(QLatin1Char(' '))) {
|
||||
return false;
|
||||
}
|
||||
if (t.contains(QLatin1String(".."))) {
|
||||
return false;
|
||||
}
|
||||
if (!t.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression legalPartial(QStringLiteral(R"(^[a-zA-Z0-9.-]*$)"));
|
||||
if (!legalPartial.match(t).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MtProxyConfigModel::isValidOptionalIpv4(const QString &ip) const {
|
||||
const QString t = ip.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return NetworkUtilities::checkIPv4Format(t);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MtProxyConfigModel::roleNames() const {
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[PortRole] = "port";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[TagRole] = "tag";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
roles[PublicHostRole] = "publicHost";
|
||||
roles[TransportModeRole] = "transportMode";
|
||||
roles[TlsDomainRole] = "tlsDomain";
|
||||
roles[AdditionalSecretsRole] = "additionalSecrets";
|
||||
roles[WorkersModeRole] = "workersMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[NatEnabledRole] = "natEnabled";
|
||||
roles[NatInternalIpRole] = "natInternalIp";
|
||||
roles[NatExternalIpRole] = "natExternalIp";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
amnezia::MtProxyProtocolConfig MtProxyConfigModel::getProtocolConfig() {
|
||||
return m_protocolConfig;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
#ifndef MTPROXYCONFIGMODEL_H
|
||||
#define MTPROXYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
|
||||
class MtProxyConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
SecretRole,
|
||||
TagRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole,
|
||||
PublicHostRole,
|
||||
TransportModeRole,
|
||||
TlsDomainRole,
|
||||
AdditionalSecretsRole,
|
||||
WorkersModeRole,
|
||||
WorkersRole,
|
||||
NatEnabledRole,
|
||||
NatInternalIpRole,
|
||||
NatExternalIpRole
|
||||
};
|
||||
|
||||
explicit MtProxyConfigModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
public slots:
|
||||
|
||||
void updateModel(amnezia::DockerContainer container, const amnezia::MtProxyProtocolConfig &protocolConfig);
|
||||
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
|
||||
amnezia::MtProxyProtocolConfig getProtocolConfig();
|
||||
|
||||
Q_INVOKABLE void generateSecret();
|
||||
|
||||
Q_INVOKABLE void setSecret(const QString &secret);
|
||||
|
||||
Q_INVOKABLE bool validateAndSetSecret(const QString &rawSecret);
|
||||
|
||||
Q_INVOKABLE void setPort(const QString &port);
|
||||
|
||||
Q_INVOKABLE void setTag(const QString &tag);
|
||||
|
||||
Q_INVOKABLE void setPublicHost(const QString &host);
|
||||
|
||||
Q_INVOKABLE void setTransportMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE QString getTransportMode() const;
|
||||
|
||||
Q_INVOKABLE QString getTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString getPublicHost() const;
|
||||
|
||||
Q_INVOKABLE void setTlsDomain(const QString &domain);
|
||||
|
||||
Q_INVOKABLE void setWorkersMode(const QString &mode);
|
||||
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
|
||||
Q_INVOKABLE void setNatEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE void setNatInternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void setNatExternalIp(const QString &ip);
|
||||
|
||||
Q_INVOKABLE void addAdditionalSecret();
|
||||
|
||||
Q_INVOKABLE void removeAdditionalSecret(int idx);
|
||||
/// Current `mtproxy_additional_secrets` list from in-memory config (for QML snapshot vs. unsaved adds).
|
||||
Q_INVOKABLE QVariantList additionalSecretsList() const;
|
||||
|
||||
Q_INVOKABLE QString generateQrCode(const QString &text);
|
||||
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE QString defaultTlsDomain() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeStandard() const;
|
||||
|
||||
Q_INVOKABLE QString transportModeFakeTLS() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeAuto() const;
|
||||
|
||||
Q_INVOKABLE QString workersModeManual() const;
|
||||
|
||||
Q_INVOKABLE bool isValidPublicHost(const QString &host) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isPublicHostTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidMtProxyTag(const QString &tag) const;
|
||||
|
||||
Q_INVOKABLE bool isMtProxyTagTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE int mtProxyBotTagHexLength() const;
|
||||
|
||||
Q_INVOKABLE bool isValidFakeTlsDomain(const QString &domain) const;
|
||||
|
||||
Q_INVOKABLE QString normalizeFakeTlsDomainInput(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeFakeTlsDomainFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainInputAllowed(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE QString clipboardText() const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePublicHostFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeMtProxyTagFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeOptionalIpv4FieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isFakeTlsDomainTypingIncomplete(const QString &text) const;
|
||||
|
||||
Q_INVOKABLE bool isValidOptionalIpv4(const QString &ip) const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
amnezia::DockerContainer m_container;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::MtProxyProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // MTPROXYCONFIGMODEL_H
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "mtproxy_public_host_input.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ipv4OctetTokenOk(const QString &s) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^\d{1,3}$)"));
|
||||
if (!re.match(s).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = s.toInt(&ok);
|
||||
return ok && n >= 0 && n <= 255;
|
||||
}
|
||||
|
||||
// Reject labels like "312edweqwe" (digits >255 then letters).
|
||||
bool labelHasInvalidOctetLikePrefixBeforeLetters(const QString &label) {
|
||||
static const QRegularExpression re(QStringLiteral(R"(^(\d+)([a-zA-Z].*)$)"));
|
||||
const QRegularExpressionMatch m = re.match(label);
|
||||
if (!m.hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
const QString digits = m.captured(1);
|
||||
if (digits.length() > 3) {
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = digits.toInt(&ok);
|
||||
if (!ok) {
|
||||
return true;
|
||||
}
|
||||
if (n > 255) {
|
||||
return true;
|
||||
}
|
||||
// Do not restrict n≤255 + letters here (e.g. "123mlkjh.example.com"); four-segment IPv4+junk is handled below.
|
||||
return false;
|
||||
}
|
||||
|
||||
// "123.123wqqweqweqweqwe" — first label is a real octet, second looks like an octet glued to letters (not "123.45").
|
||||
bool looksLikeTwoSegmentOctetThenDigitLetterGlue(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 2) {
|
||||
return false;
|
||||
}
|
||||
if (!ipv4OctetTokenOk(parts.at(0))) {
|
||||
return false;
|
||||
}
|
||||
const QString &p1 = parts.at(1);
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
if (!digitThenLetter.match(p1).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return !ipv4OctetTokenOk(p1);
|
||||
}
|
||||
|
||||
// "a.b.c.djunk" where first three parts are pure octets and last part has digits then letters (e.g. "123wdqweqweqwe").
|
||||
bool looksLikeFourOctetIpv4WithGarbageInLastSegment(const QString &text) {
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
if (parts.size() != 4) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!ipv4OctetTokenOk(parts.at(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const QRegularExpression digitThenLetter(QStringLiteral(R"(^\d+[a-zA-Z])"));
|
||||
return digitThenLetter.match(parts.at(3)).hasMatch();
|
||||
}
|
||||
|
||||
bool hostLabelsRejectBrokenDigitLetterMix(const QString &text) {
|
||||
if (looksLikeTwoSegmentOctetThenDigitLetterGlue(text)) {
|
||||
return false;
|
||||
}
|
||||
if (looksLikeFourOctetIpv4WithGarbageInLastSegment(text)) {
|
||||
return false;
|
||||
}
|
||||
const QStringList parts = text.split(QLatin1Char('.'), Qt::KeepEmptyParts);
|
||||
for (const QString &part: parts) {
|
||||
if (labelHasInvalidOctetLikePrefixBeforeLetters(part)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text) {
|
||||
if (text.length() > 253) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression allowed(QStringLiteral(R"(^[a-zA-Z0-9.:\-]*$)"));
|
||||
if (!allowed.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigits(QStringLiteral(R"(^\d+$)"));
|
||||
if (onlyDigits.match(text).hasMatch() && text.length() > 3) {
|
||||
return false;
|
||||
}
|
||||
static const QRegularExpression onlyDigitDot(QStringLiteral(R"(^[0-9.]+$)"));
|
||||
if (!text.isEmpty() && onlyDigitDot.match(text).hasMatch()) {
|
||||
static const QRegularExpression ipv4Partial(QStringLiteral(R"(^(\d{1,3}\.){0,3}\d{0,3}$)"));
|
||||
return ipv4Partial.match(text).hasMatch();
|
||||
}
|
||||
if (text.contains(QLatin1Char(':'))) {
|
||||
static const QRegularExpression ipv6Chars(QStringLiteral(R"(^[0-9a-fA-F:.]*$)"));
|
||||
if (!ipv6Chars.match(text).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
if (text.size() > 45) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!hostLabelsRejectBrokenDigitLetterMix(text)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PublicHostInputValidator::PublicHostInputValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State PublicHostInputValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos)
|
||||
return mtproxyPublicHostInputAllowed(input) ? Acceptable : Invalid;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
#define MTPROXY_PUBLIC_HOST_INPUT_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <QValidator>
|
||||
|
||||
/// Shared rules for public host field (IPv4 dotted partial, IPv6 hex, FQDN ASCII).
|
||||
bool mtproxyPublicHostInputAllowed(const QString &text);
|
||||
|
||||
class PublicHostInputValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PublicHostInputValidator(QObject *parent = nullptr);
|
||||
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user