mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-08-25 04:29:14 +03:00
Compare commits
6 Commits
dev
...
feat/add-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee87aff848 | ||
|
|
d0f3856c8f | ||
|
|
1142d7a898 | ||
|
|
9fd142fca4 | ||
|
|
15b384aac8 | ||
|
|
04d6ff1631 |
@@ -37,6 +37,7 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/installers/tProxyInstaller.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
|
||||
@@ -120,6 +121,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/installers/tProxyInstaller.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
|
||||
|
||||
@@ -113,6 +113,9 @@ void CoreController::initModels()
|
||||
m_telemtConfigModel = new TelemtConfigModel(this);
|
||||
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
|
||||
|
||||
m_tProxyConfigModel = new TProxyConfigModel(this);
|
||||
setQmlContextProperty("TProxyConfigModel", m_tProxyConfigModel);
|
||||
|
||||
m_clientManagementModel = new ClientManagementModel(this);
|
||||
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
|
||||
|
||||
@@ -183,6 +186,7 @@ void CoreController::initControllers()
|
||||
m_ikev2ConfigModel,
|
||||
#endif
|
||||
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
|
||||
m_tProxyConfigModel,
|
||||
m_connectionController, this);
|
||||
setQmlContextProperty("InstallController", m_installUiController);
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
#include "ui/models/services/tProxyConfigModel.h"
|
||||
|
||||
#include "ui/models/ipSplitTunnelingModel.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
@@ -226,6 +227,7 @@ private:
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
TProxyConfigModel* m_tProxyConfigModel;
|
||||
|
||||
CoreSignalHandlers* m_signalHandlers;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "core/installers/socks5Installer.h"
|
||||
#include "core/installers/mtProxyInstaller.h"
|
||||
#include "core/installers/telemtInstaller.h"
|
||||
#include "core/installers/tProxyInstaller.h"
|
||||
#include "core/installers/torInstaller.h"
|
||||
#include "core/installers/wireguardInstaller.h"
|
||||
#include "core/installers/xrayInstaller.h"
|
||||
@@ -103,7 +104,6 @@ InstallController::~InstallController()
|
||||
ErrorCode InstallController::setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config,
|
||||
bool isUpdate)
|
||||
{
|
||||
qDebug().noquote() << "InstallController::setupContainer" << ContainerUtils::containerToString(container);
|
||||
SshSession sshSession;
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
|
||||
@@ -118,7 +118,6 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
e = installDockerWorker(credentials, container, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer installDockerWorker finished";
|
||||
|
||||
if (!isUpdate) {
|
||||
e = isServerPortBusy(credentials, container, config, sshSession);
|
||||
@@ -129,29 +128,24 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
e = prepareHostWorker(credentials, container, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer prepareHostWorker finished";
|
||||
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
|
||||
const bool removeDataVolume = !isUpdate && (container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy);
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
qDebug().noquote() << "InstallController::setupContainer removeContainer finished";
|
||||
|
||||
qDebug().noquote() << "buildContainerWorker start";
|
||||
e = buildContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer buildContainerWorker finished";
|
||||
|
||||
e = runContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer runContainerWorker finished";
|
||||
|
||||
e = configureContainerWorker(credentials, container, config, sshSession);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer configureContainerWorker finished";
|
||||
|
||||
if (container == DockerContainer::Xray || container == DockerContainer::SSXray) {
|
||||
DnsSettings dnsSettings = { m_appSettingsRepository->primaryDns(), m_appSettingsRepository->secondaryDns() };
|
||||
@@ -159,11 +153,9 @@ ErrorCode InstallController::setupContainer(const ServerCredentials &credentials
|
||||
e = xrayConfigurator.writeServerConfigForSetup(credentials, container, config, dnsSettings);
|
||||
if (e)
|
||||
return e;
|
||||
qDebug().noquote() << "InstallController::setupContainer xray writeServerConfigForSetup finished";
|
||||
}
|
||||
|
||||
setupServerFirewall(credentials, sshSession);
|
||||
qDebug().noquote() << "InstallController::setupContainer setupServerFirewall finished";
|
||||
|
||||
return startupContainerWorker(credentials, container, config, sshSession);
|
||||
}
|
||||
@@ -184,6 +176,10 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
ServerCredentials credentials = adminConfig->credentials();
|
||||
SshSession sshSession;
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
adminConfig->updateContainerConfig(container, newConfig);
|
||||
m_serversRepository->editServer(serverId, adminConfig->toJson(), serverConfigUtils::ConfigType::SelfHostedAdmin);
|
||||
@@ -220,7 +216,8 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError
|
||||
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt)) {
|
||||
&& (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy)) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
errorCode = sshSession.runScript(credentials, "sudo docker restart " + containerName);
|
||||
}
|
||||
@@ -231,6 +228,8 @@ ErrorCode InstallController::updateServerConfig(const QString &serverId, DockerC
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, newConfig);
|
||||
}
|
||||
if (reinstallRequired) {
|
||||
clearCachedProfile(serverId, container);
|
||||
@@ -577,6 +576,8 @@ ErrorCode InstallController::configureContainerWorker(const ServerCredentials &c
|
||||
MtProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
TelemtInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
TProxyInstaller::uploadClientSettingsSnapshot(sshSession, credentials, container, config);
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -628,22 +629,42 @@ ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentia
|
||||
if (port.isEmpty()) {
|
||||
port = QString::number(ProtocolUtils::defaultPort(protocol));
|
||||
}
|
||||
if (container == DockerContainer::TProxy) {
|
||||
if (const auto *tProxyConfig = config.getTProxyProtocolConfig()) {
|
||||
const QString httpPort =
|
||||
tProxyConfig->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : tProxyConfig->httpPort;
|
||||
if (!fixedPorts.contains(httpPort) && httpPort != port) {
|
||||
fixedPorts.append(httpPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
QString transportProto = config.protocolConfig.transportProto();
|
||||
if (transportProto.isEmpty()) {
|
||||
transportProto = ProtocolUtils::transportProtoToString(ProtocolUtils::defaultTransportProto(protocol), protocol);
|
||||
}
|
||||
|
||||
// TODO reimplement with netstat
|
||||
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E ':%1 ").arg(port);
|
||||
for (auto &port : fixedPorts) {
|
||||
script = script.append("|:%1").arg(port);
|
||||
// Match exact host ports in lsof output (e.g. *:80) but not prefixes like *:8025 or *:8080.
|
||||
QStringList portsToCheck;
|
||||
portsToCheck << port;
|
||||
for (const QString &fixedPort : fixedPorts) {
|
||||
if (!portsToCheck.contains(fixedPort)) {
|
||||
portsToCheck << fixedPort;
|
||||
}
|
||||
}
|
||||
QStringList portRegexParts;
|
||||
for (const QString &p : portsToCheck) {
|
||||
portRegexParts << QString(":%1([^0-9]|$)").arg(p);
|
||||
}
|
||||
const QString portRegex = portRegexParts.join(QLatin1Char('|'));
|
||||
|
||||
QString script = QString("which lsof > /dev/null 2>&1 || true && sudo lsof -i -P -n 2>/dev/null | grep -E '%1'")
|
||||
.arg(portRegex);
|
||||
|
||||
if (transportProto == "tcpandudp") {
|
||||
QString tcpProtoScript = script;
|
||||
QString udpProtoScript = script;
|
||||
tcpProtoScript.append("' | grep -i tcp");
|
||||
udpProtoScript.append("' | grep -i udp");
|
||||
tcpProtoScript.append(" | grep -i tcp");
|
||||
udpProtoScript.append(" | grep -i udp");
|
||||
tcpProtoScript.append(" | grep LISTEN");
|
||||
|
||||
ErrorCode errorCode = sshSession.runScript(
|
||||
@@ -668,7 +689,7 @@ ErrorCode InstallController::isServerPortBusy(const ServerCredentials &credentia
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
script = script.append("' | grep -i %1").arg(transportProto);
|
||||
script = script.append(" | grep -i %1").arg(transportProto);
|
||||
|
||||
if (transportProto == "tcp") {
|
||||
script = script.append(" | grep LISTEN");
|
||||
@@ -761,6 +782,24 @@ bool InstallController::isReinstallContainerRequired(DockerContainer container,
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::TProxy) {
|
||||
const auto *oldP = oldConfig.getTProxyProtocolConfig();
|
||||
const auto *newP = newConfig.getTProxyProtocolConfig();
|
||||
if (oldP && newP) {
|
||||
const QString oldHttps =
|
||||
oldP->port.isEmpty() ? QString(protocols::tProxy::defaultPort) : oldP->port;
|
||||
const QString newHttps =
|
||||
newP->port.isEmpty() ? QString(protocols::tProxy::defaultPort) : newP->port;
|
||||
const QString oldHttp =
|
||||
oldP->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : oldP->httpPort;
|
||||
const QString newHttp =
|
||||
newP->httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : newP->httpPort;
|
||||
if (oldHttps != newHttps || oldHttp != newHttp) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Socks5Proxy) {
|
||||
return true;
|
||||
}
|
||||
@@ -797,7 +836,8 @@ ErrorCode InstallController::installDockerWorker(const ServerCredentials &creden
|
||||
|
||||
qDebug().noquote() << "InstallController::installDockerWorker" << stdOut;
|
||||
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy) {
|
||||
QString conntrackOut;
|
||||
auto cbConntrack = [&](const QString &data, libssh::Client &) {
|
||||
conntrackOut += data + "\n";
|
||||
@@ -1010,7 +1050,8 @@ ErrorCode InstallController::removeContainer(const QString &serverId, DockerCont
|
||||
SshSession sshSession;
|
||||
const amnezia::ScriptVars removeContainerVars =
|
||||
amnezia::genBaseVars(credentials, container, QString(), QString());
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt);
|
||||
const bool removeDataVolume = (container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy);
|
||||
ErrorCode errorCode =
|
||||
sshSession.runScript(credentials, buildRemoveContainerScript(removeContainerVars, removeDataVolume));
|
||||
|
||||
@@ -1049,6 +1090,7 @@ QScopedPointer<InstallerBase> InstallController::createInstaller(DockerContainer
|
||||
case DockerContainer::Socks5Proxy: return QScopedPointer<InstallerBase>(new Socks5Installer(this));
|
||||
case DockerContainer::MtProxy: return QScopedPointer<InstallerBase>(new MtProxyInstaller(this));
|
||||
case DockerContainer::Telemt: return QScopedPointer<InstallerBase>(new TelemtInstaller(this));
|
||||
case DockerContainer::TProxy: return QScopedPointer<InstallerBase>(new TProxyInstaller(this));
|
||||
default: return QScopedPointer<InstallerBase>(new InstallerBase(this));
|
||||
}
|
||||
}
|
||||
@@ -1063,9 +1105,35 @@ ErrorCode InstallController::installContainer(const ServerCredentials &credentia
|
||||
TransportProto transportProto, ContainerConfig &config)
|
||||
{
|
||||
config = generateConfig(container, port, transportProto);
|
||||
if (container == DockerContainer::TProxy) {
|
||||
auto *tProxyConfig = config.getTProxyProtocolConfig();
|
||||
if (tProxyConfig) {
|
||||
if (!m_tproxyInstallHostname.isEmpty()) {
|
||||
tProxyConfig->hostname = m_tproxyInstallHostname;
|
||||
}
|
||||
if (!m_tproxyInstallEmail.isEmpty()) {
|
||||
tProxyConfig->acmeEmail = m_tproxyInstallEmail;
|
||||
}
|
||||
}
|
||||
m_tproxyInstallHostname.clear();
|
||||
m_tproxyInstallEmail.clear();
|
||||
|
||||
// TProxy needs a hostname and ACME email before the first deploy (configure_container.sh
|
||||
// exits 1 without them). Fail fast with a clear error instead of a confusing server-side
|
||||
// failure if the install path is reached without them being supplied via setTProxyInstallHints().
|
||||
if (!tProxyConfig || tProxyConfig->hostname.isEmpty() || tProxyConfig->acmeEmail.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
return setupContainer(credentials, container, config, false);
|
||||
}
|
||||
|
||||
void InstallController::setTProxyInstallHints(const QString &hostname, const QString &email)
|
||||
{
|
||||
m_tproxyInstallHostname = hostname;
|
||||
m_tproxyInstallEmail = email;
|
||||
}
|
||||
|
||||
|
||||
bool InstallController::isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig)
|
||||
{
|
||||
@@ -1101,6 +1169,13 @@ bool InstallController::isUpdateDockerContainerRequired(DockerContainer containe
|
||||
return true;
|
||||
}
|
||||
return !oldT->equalsDockerDeploymentSettings(*newT);
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
const auto *oldP = oldConfig.getTProxyProtocolConfig();
|
||||
const auto *newP = newConfig.getTProxyProtocolConfig();
|
||||
if (!oldP || !newP) {
|
||||
return true;
|
||||
}
|
||||
return !oldP->equalsDockerDeploymentSettings(*newP);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1452,6 +1527,34 @@ void InstallController::updateContainerConfigAfterInstallation(DockerContainer c
|
||||
telemtConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
}
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
if (auto *tProxyConfig = containerConfig.getTProxyProtocolConfig()) {
|
||||
static const QRegularExpression reSecret(
|
||||
QStringLiteral(R"(\[\*\]\s+Secret:\s+([0-9a-fA-F]{32}))"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression reTgLink(QStringLiteral(R"(\[\*\]\s+tg://\s+link:\s+(tg://webproxy\?[^\s]+))"));
|
||||
static const QRegularExpression reTmeLink(
|
||||
QStringLiteral(R"(\[\*\]\s+t\.me\s+link:\s+(https://t\.me/webproxy\?[^\s]+))"));
|
||||
static const QRegularExpression reHost(QStringLiteral(R"(\[\*\]\s+Hostname:\s+(\S+))"));
|
||||
|
||||
const QRegularExpressionMatch mSecret = reSecret.match(stdOut);
|
||||
const QRegularExpressionMatch mTgLink = reTgLink.match(stdOut);
|
||||
const QRegularExpressionMatch mTmeLink = reTmeLink.match(stdOut);
|
||||
const QRegularExpressionMatch mHost = reHost.match(stdOut);
|
||||
|
||||
if (mSecret.hasMatch()) {
|
||||
tProxyConfig->secret = mSecret.captured(1);
|
||||
}
|
||||
if (mTgLink.hasMatch()) {
|
||||
tProxyConfig->tgLink = mTgLink.captured(1);
|
||||
}
|
||||
if (mTmeLink.hasMatch()) {
|
||||
tProxyConfig->tmeLink = mTmeLink.captured(1);
|
||||
}
|
||||
if (mHost.hasMatch() && tProxyConfig->hostname.isEmpty()) {
|
||||
tProxyConfig->hostname = mHost.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1499,6 +1602,11 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
auto installer = createInstaller(container);
|
||||
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
|
||||
if (container == DockerContainer::TProxy) {
|
||||
if (auto *tProxyConfig = config.getTProxyProtocolConfig()) {
|
||||
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
|
||||
}
|
||||
}
|
||||
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
|
||||
|
||||
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
|
||||
@@ -1524,6 +1632,11 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
auto installer = createInstaller(container);
|
||||
ContainerConfig config = installer->createBaseConfig(container, port, transportProto);
|
||||
if (container == DockerContainer::TProxy) {
|
||||
if (auto *tProxyConfig = config.getTProxyProtocolConfig()) {
|
||||
TProxyInstaller::applyDockerPublishedPorts(containerInfo, *tProxyConfig);
|
||||
}
|
||||
}
|
||||
ErrorCode extractError = installer->extractConfigFromContainer(container, credentials, &sshSession, config);
|
||||
|
||||
if (extractError != ErrorCode::NoError && extractError != ErrorCode::ServerContainerMissingError) {
|
||||
@@ -1539,7 +1652,8 @@ ErrorCode InstallController::getAlreadyInstalledContainers(const ServerCredentia
|
||||
|
||||
ErrorCode InstallController::setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
|
||||
&& container != DockerContainer::TProxy) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
@@ -1566,6 +1680,9 @@ ErrorCode InstallController::setDockerContainerEnabledState(const QString &serve
|
||||
} else if (auto *telemtConfig = currentConfig.getTelemtProtocolConfig()) {
|
||||
telemtConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
} else if (auto *tProxyConfig = currentConfig.getTProxyProtocolConfig()) {
|
||||
tProxyConfig->isEnabled = enabled;
|
||||
persist = true;
|
||||
}
|
||||
if (persist) {
|
||||
adminConfig->updateContainerConfig(container, currentConfig);
|
||||
@@ -1631,7 +1748,8 @@ ErrorCode InstallController::queryMtProxyDiagnostics(const QString &serverId, Do
|
||||
|
||||
QString InstallController::fetchDockerContainerSecret(const QString &serverId, DockerContainer container)
|
||||
{
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
|
||||
&& container != DockerContainer::TProxy) {
|
||||
return {};
|
||||
}
|
||||
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
|
||||
|
||||
@@ -55,6 +55,8 @@ public:
|
||||
|
||||
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
|
||||
|
||||
void setTProxyInstallHints(const QString &hostname, const QString &email);
|
||||
|
||||
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
|
||||
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
|
||||
|
||||
@@ -126,6 +128,8 @@ private:
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
bool m_cancelInstallation = false;
|
||||
QString m_tproxyInstallHostname;
|
||||
QString m_tproxyInstallEmail;
|
||||
|
||||
#ifndef Q_OS_IOS
|
||||
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/ikev2ProtocolConfig.h"
|
||||
#include "core/models/protocols/torProtocolConfig.h"
|
||||
|
||||
@@ -115,6 +116,15 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
|
||||
config.protocolConfig = telemtConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::TProxy: {
|
||||
TProxyProtocolConfig tProxyConfig;
|
||||
tProxyConfig.port = portStr.isEmpty() ? QString(protocols::tProxy::defaultPort) : portStr;
|
||||
tProxyConfig.httpPort = QString(protocols::tProxy::defaultHttpPort);
|
||||
tProxyConfig.carrierMode = QString(protocols::tProxy::carrierModeHttps);
|
||||
tProxyConfig.workers = QString(protocols::tProxy::defaultWorkers);
|
||||
config.protocolConfig = tProxyConfig;
|
||||
break;
|
||||
}
|
||||
case Proto::Ikev2: {
|
||||
Ikev2ProtocolConfig ikev2Config;
|
||||
config.protocolConfig = ikev2Config;
|
||||
|
||||
@@ -162,6 +162,8 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
|
||||
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
|
||||
const QString containerName = ContainerUtils::containerToString(container);
|
||||
const bool isTelemt = container == DockerContainer::Telemt;
|
||||
const QString confFile =
|
||||
isTelemt ? QStringLiteral("/data/config.toml") : QStringLiteral("/data/proxy-multi.conf");
|
||||
|
||||
const QString sportFilter = QString::number(listenPort);
|
||||
const QString peersCmd = QStringLiteral("sudo conntrack -L -p tcp --dport ") + sportFilter
|
||||
@@ -172,8 +174,6 @@ ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const Serve
|
||||
"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("; ")
|
||||
|
||||
141
client/core/installers/tProxyInstaller.cpp
Normal file
141
client/core/installers/tProxyInstaller.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "tProxyInstaller.h"
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
#include "core/utils/selfhosted/sshSession.h"
|
||||
#include "core/models/containerConfig.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
constexpr QLatin1String kTProxyClientJsonPath("/data/amnezia-tproxy-client.json");
|
||||
constexpr QLatin1String kTProxyClientJsonUploadPath("data/amnezia-tproxy-client.json");
|
||||
constexpr QLatin1String kTProxySecretPath("/data/secret");
|
||||
constexpr QLatin1String kTProxyMetaPath("/data/tproxy-meta");
|
||||
|
||||
void rebuildTProxyLinksIfNeeded(TProxyProtocolConfig *tc)
|
||||
{
|
||||
if (!tc || tc->hostname.isEmpty() || tc->secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString host = tc->hostname;
|
||||
const QString sec = tc->secret;
|
||||
// WEB proxy type fixes HTTPS/443; the canonical webproxy link carries no port.
|
||||
if (tc->tgLink.isEmpty()) {
|
||||
tc->tgLink = QStringLiteral("tg://webproxy?server=%1&secret=%2").arg(host, sec);
|
||||
}
|
||||
if (tc->tmeLink.isEmpty()) {
|
||||
tc->tmeLink = QStringLiteral("https://t.me/webproxy?server=%1&secret=%2").arg(host, sec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TProxyInstaller::applyDockerPublishedPorts(const QString &dockerPsPortsLine, TProxyProtocolConfig &config)
|
||||
{
|
||||
static const QRegularExpression httpRe(QStringLiteral(":(\\d+)->80/(tcp|udp)"));
|
||||
static const QRegularExpression httpsRe(QStringLiteral(":(\\d+)->443/(tcp|udp)"));
|
||||
const QRegularExpressionMatch httpMatch = httpRe.match(dockerPsPortsLine);
|
||||
const QRegularExpressionMatch httpsMatch = httpsRe.match(dockerPsPortsLine);
|
||||
if (httpsMatch.hasMatch()) {
|
||||
config.port = httpsMatch.captured(1);
|
||||
}
|
||||
if (httpMatch.hasMatch()) {
|
||||
config.httpPort = httpMatch.captured(1);
|
||||
}
|
||||
}
|
||||
|
||||
TProxyInstaller::TProxyInstaller(QObject *parent) : InstallerBase(parent) {}
|
||||
|
||||
ErrorCode TProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
SshSession *sshSession, ContainerConfig &config)
|
||||
{
|
||||
if (container != DockerContainer::TProxy || !sshSession) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
TProxyProtocolConfig *tc = config.getTProxyProtocolConfig();
|
||||
if (!tc) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode jsonErr = ErrorCode::NoError;
|
||||
const QByteArray jsonRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxyClientJsonPath), jsonErr);
|
||||
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
QJsonObject merged = tc->toJson();
|
||||
const QJsonObject snap = doc.object();
|
||||
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
|
||||
merged.insert(it.key(), it.value());
|
||||
}
|
||||
*tc = TProxyProtocolConfig::fromJson(merged);
|
||||
}
|
||||
}
|
||||
|
||||
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
ErrorCode secretErr = ErrorCode::NoError;
|
||||
const QByteArray secretRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxySecretPath), secretErr);
|
||||
const QString sec = QString::fromUtf8(secretRaw).trimmed();
|
||||
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
|
||||
tc->secret = sec;
|
||||
}
|
||||
|
||||
ErrorCode metaErr = ErrorCode::NoError;
|
||||
const QByteArray metaRaw =
|
||||
sshSession->getTextFileFromContainer(container, credentials, QString(kTProxyMetaPath), metaErr);
|
||||
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
|
||||
const QList<QByteArray> lines = metaRaw.split('\n');
|
||||
for (const QByteArray &rawLine : lines) {
|
||||
const QString line = QString::fromUtf8(rawLine).trimmed();
|
||||
const int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
const QString key = line.left(eq);
|
||||
const QString val = line.mid(eq + 1).trimmed();
|
||||
if (key == QLatin1String("hostname") && tc->hostname.isEmpty()) {
|
||||
tc->hostname = val;
|
||||
} else if (key == QLatin1String("email") && tc->acmeEmail.isEmpty()) {
|
||||
tc->acmeEmail = val;
|
||||
} else if (key == QLatin1String("carrier") && tc->carrierMode.isEmpty()) {
|
||||
tc->carrierMode = val;
|
||||
} else if (key == QLatin1String("workers") && tc->workers.isEmpty()) {
|
||||
tc->workers = val;
|
||||
} else if (key == QLatin1String("http_port") && !val.isEmpty()) {
|
||||
tc->httpPort = val;
|
||||
} else if (key == QLatin1String("https_port") && !val.isEmpty()) {
|
||||
tc->port = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rebuildTProxyLinksIfNeeded(tc);
|
||||
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void TProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
|
||||
DockerContainer container, const ContainerConfig &config)
|
||||
{
|
||||
const TProxyProtocolConfig *tc = config.getTProxyProtocolConfig();
|
||||
if (!tc) {
|
||||
return;
|
||||
}
|
||||
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
|
||||
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
|
||||
QString(kTProxyClientJsonUploadPath));
|
||||
if (err != ErrorCode::NoError) {
|
||||
qWarning() << "TProxyInstaller::uploadClientSettingsSnapshot failed" << err;
|
||||
}
|
||||
}
|
||||
25
client/core/installers/tProxyInstaller.h
Normal file
25
client/core/installers/tProxyInstaller.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef TPROXYINSTALLER_H
|
||||
#define TPROXYINSTALLER_H
|
||||
|
||||
#include "installerBase.h"
|
||||
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
class TProxyInstaller : public InstallerBase {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TProxyInstaller(QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode
|
||||
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
|
||||
SshSession *sshSession, amnezia::ContainerConfig &config) override;
|
||||
|
||||
static void applyDockerPublishedPorts(const QString &dockerPsPortsLine,
|
||||
amnezia::TProxyProtocolConfig &config);
|
||||
|
||||
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container,
|
||||
const amnezia::ContainerConfig &config);
|
||||
};
|
||||
|
||||
#endif // TPROXYINSTALLER_H
|
||||
@@ -133,6 +133,16 @@ const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
|
||||
return protocolConfig.as<TelemtProtocolConfig>();
|
||||
}
|
||||
|
||||
TProxyProtocolConfig* ContainerConfig::getTProxyProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<TProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
const TProxyProtocolConfig* ContainerConfig::getTProxyProtocolConfig() const
|
||||
{
|
||||
return protocolConfig.as<TProxyProtocolConfig>();
|
||||
}
|
||||
|
||||
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
|
||||
{
|
||||
return protocolConfig.as<Ikev2ProtocolConfig>();
|
||||
|
||||
@@ -63,6 +63,9 @@ struct ContainerConfig {
|
||||
TelemtProtocolConfig* getTelemtProtocolConfig();
|
||||
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
|
||||
|
||||
TProxyProtocolConfig* getTProxyProtocolConfig();
|
||||
const TProxyProtocolConfig* getTProxyProtocolConfig() const;
|
||||
|
||||
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
|
||||
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -44,6 +45,8 @@ Proto ProtocolConfig::type() const
|
||||
return Proto::MtProxy;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return Proto::Telemt;
|
||||
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
|
||||
return Proto::TProxy;
|
||||
}
|
||||
return Proto::Unknown;
|
||||
}, data);
|
||||
@@ -75,6 +78,8 @@ QString ProtocolConfig::port() const
|
||||
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
|
||||
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
|
||||
return arg.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : arg.port;
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -102,6 +107,8 @@ QString ProtocolConfig::transportProto() const
|
||||
return QStringLiteral("tcp");
|
||||
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
} else if constexpr (std::is_same_v<T, TProxyProtocolConfig>) {
|
||||
return QStringLiteral("tcp");
|
||||
}
|
||||
return QString();
|
||||
}, data);
|
||||
@@ -317,6 +324,8 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
|
||||
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
|
||||
case Proto::Telemt:
|
||||
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
|
||||
case Proto::TProxy:
|
||||
return ProtocolConfig{TProxyProtocolConfig::fromJson(json)};
|
||||
default:
|
||||
return ProtocolConfig{AwgProtocolConfig{}};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "core/models/protocols/dnsProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
namespace amnezia
|
||||
{
|
||||
@@ -40,6 +41,7 @@ struct ProtocolConfig {
|
||||
Socks5ProxyProtocolConfig,
|
||||
MtProxyProtocolConfig,
|
||||
TelemtProtocolConfig,
|
||||
TProxyProtocolConfig,
|
||||
Ikev2ProtocolConfig,
|
||||
TorProtocolConfig,
|
||||
DnsProtocolConfig
|
||||
|
||||
77
client/core/models/protocols/tProxyProtocolConfig.cpp
Normal file
77
client/core/models/protocols/tProxyProtocolConfig.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "tProxyProtocolConfig.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
QJsonObject TProxyProtocolConfig::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!port.isEmpty()) {
|
||||
obj[QString(configKey::port)] = port;
|
||||
}
|
||||
if (!httpPort.isEmpty()) {
|
||||
obj[protocols::tProxy::httpPortKey] = httpPort;
|
||||
}
|
||||
if (!secret.isEmpty()) {
|
||||
obj[protocols::tProxy::secretKey] = secret;
|
||||
}
|
||||
if (!hostname.isEmpty()) {
|
||||
obj[protocols::tProxy::hostnameKey] = hostname;
|
||||
}
|
||||
if (!acmeEmail.isEmpty()) {
|
||||
obj[protocols::tProxy::acmeEmailKey] = acmeEmail;
|
||||
}
|
||||
if (!carrierMode.isEmpty()) {
|
||||
obj[protocols::tProxy::carrierModeKey] = carrierMode;
|
||||
}
|
||||
if (!workers.isEmpty()) {
|
||||
obj[protocols::tProxy::workersKey] = workers;
|
||||
}
|
||||
if (!tgLink.isEmpty()) {
|
||||
obj[protocols::tProxy::tgLinkKey] = tgLink;
|
||||
}
|
||||
if (!tmeLink.isEmpty()) {
|
||||
obj[protocols::tProxy::tmeLinkKey] = tmeLink;
|
||||
}
|
||||
obj[protocols::tProxy::isEnabledKey] = isEnabled;
|
||||
return obj;
|
||||
}
|
||||
|
||||
TProxyProtocolConfig TProxyProtocolConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
TProxyProtocolConfig c;
|
||||
c.port = json.value(QString(configKey::port)).toString();
|
||||
c.httpPort = json.value(protocols::tProxy::httpPortKey).toString();
|
||||
c.secret = json.value(protocols::tProxy::secretKey).toString();
|
||||
c.hostname = json.value(protocols::tProxy::hostnameKey).toString();
|
||||
c.acmeEmail = json.value(protocols::tProxy::acmeEmailKey).toString();
|
||||
c.carrierMode = json.value(protocols::tProxy::carrierModeKey).toString();
|
||||
c.workers = json.value(protocols::tProxy::workersKey).toString();
|
||||
c.tgLink = json.value(protocols::tProxy::tgLinkKey).toString();
|
||||
c.tmeLink = json.value(protocols::tProxy::tmeLinkKey).toString();
|
||||
c.isEnabled = json.value(protocols::tProxy::isEnabledKey).toBool(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool TProxyProtocolConfig::equalsDockerDeploymentSettings(const TProxyProtocolConfig &other) const
|
||||
{
|
||||
const auto normCarrier = [](const QString &m) {
|
||||
return m.isEmpty() ? QString(protocols::tProxy::carrierModeHttps) : m;
|
||||
};
|
||||
const auto normWorkers = [](const QString &w) {
|
||||
return w.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : w;
|
||||
};
|
||||
|
||||
const auto normPort = [](const QString &p, const char *defaultPort) {
|
||||
return p.isEmpty() ? QString(defaultPort) : p;
|
||||
};
|
||||
|
||||
return hostname == other.hostname && secret == other.secret && acmeEmail == other.acmeEmail
|
||||
&& normPort(port, protocols::tProxy::defaultPort) == normPort(other.port, protocols::tProxy::defaultPort)
|
||||
&& normPort(httpPort, protocols::tProxy::defaultHttpPort)
|
||||
== normPort(other.httpPort, protocols::tProxy::defaultHttpPort)
|
||||
&& normCarrier(carrierMode) == normCarrier(other.carrierMode)
|
||||
&& normWorkers(workers) == normWorkers(other.workers) && isEnabled == other.isEnabled;
|
||||
}
|
||||
28
client/core/models/protocols/tProxyProtocolConfig.h
Normal file
28
client/core/models/protocols/tProxyProtocolConfig.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef TPROXYPROTOCOLCONFIG_H
|
||||
#define TPROXYPROTOCOLCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace amnezia {
|
||||
|
||||
struct TProxyProtocolConfig {
|
||||
QString port;
|
||||
QString httpPort;
|
||||
QString secret;
|
||||
QString hostname;
|
||||
QString acmeEmail;
|
||||
QString carrierMode;
|
||||
QString workers;
|
||||
QString tgLink;
|
||||
QString tmeLink;
|
||||
bool isEnabled = true;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
static TProxyProtocolConfig fromJson(const QJsonObject &json);
|
||||
bool equalsDockerDeploymentSettings(const TProxyProtocolConfig &other) const;
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
#endif // TPROXYPROTOCOLCONFIG_H
|
||||
@@ -71,6 +71,7 @@ QMap<Proto, QString> ProtocolUtils::protocolHumanNames()
|
||||
{ Proto::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ Proto::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ Proto::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
{ Proto::TProxy, QObject::tr("TProxy (Telegram WEB)") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +98,7 @@ ServiceType ProtocolUtils::protocolService(Proto p)
|
||||
case Proto::Socks5Proxy: return ServiceType::Other;
|
||||
case Proto::MtProxy: return ServiceType::Other;
|
||||
case Proto::Telemt: return ServiceType::Other;
|
||||
case Proto::TProxy: return ServiceType::Other;
|
||||
default: return ServiceType::Other;
|
||||
}
|
||||
}
|
||||
@@ -111,6 +113,7 @@ int ProtocolUtils::getPortForInstall(Proto p)
|
||||
return QRandomGenerator::global()->bounded(30000, 50000);
|
||||
case MtProxy:
|
||||
case Telemt:
|
||||
case TProxy:
|
||||
default:
|
||||
return defaultPort(p);
|
||||
}
|
||||
@@ -132,6 +135,7 @@ int ProtocolUtils::defaultPort(Proto p)
|
||||
case Proto::Socks5Proxy: return 38080;
|
||||
case Proto::MtProxy: return QString(protocols::mtProxy::defaultPort).toInt();
|
||||
case Proto::Telemt: return QString(protocols::telemt::defaultPort).toInt();
|
||||
case Proto::TProxy: return QString(protocols::tProxy::defaultPort).toInt();
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -152,6 +156,7 @@ bool ProtocolUtils::defaultPortChangeable(Proto p)
|
||||
case Proto::Socks5Proxy: return true;
|
||||
case Proto::MtProxy: return true;
|
||||
case Proto::Telemt: return true;
|
||||
case Proto::TProxy: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -174,6 +179,7 @@ TransportProto ProtocolUtils::defaultTransportProto(Proto p)
|
||||
case Proto::Socks5Proxy: return TransportProto::Tcp;
|
||||
case Proto::MtProxy: return TransportProto::Tcp;
|
||||
case Proto::Telemt: return TransportProto::Tcp;
|
||||
case Proto::TProxy: return TransportProto::Tcp;
|
||||
default: return TransportProto::Udp;
|
||||
}
|
||||
}
|
||||
@@ -195,6 +201,7 @@ bool ProtocolUtils::defaultTransportProtoChangeable(Proto p)
|
||||
case Proto::Socks5Proxy: return false;
|
||||
case Proto::MtProxy: return false;
|
||||
case Proto::Telemt: return false;
|
||||
case Proto::TProxy: return false;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ namespace amnezia
|
||||
constexpr QLatin1String socks5proxy("socks5proxy");
|
||||
constexpr QLatin1String mtproxy("mtproxy");
|
||||
constexpr QLatin1String telemt("telemt");
|
||||
constexpr QLatin1String tproxy("tproxy");
|
||||
|
||||
constexpr QLatin1String splitTunnelSites("splitTunnelSites");
|
||||
constexpr QLatin1String splitTunnelType("splitTunnelType");
|
||||
|
||||
@@ -294,6 +294,29 @@ namespace amnezia
|
||||
constexpr int botTagHexLength = 32;
|
||||
}
|
||||
|
||||
namespace tProxy
|
||||
{
|
||||
constexpr char secretKey[] = "tproxy_secret";
|
||||
constexpr char hostnameKey[] = "tproxy_hostname";
|
||||
constexpr char acmeEmailKey[] = "tproxy_acme_email";
|
||||
constexpr char carrierModeKey[] = "tproxy_carrier_mode";
|
||||
constexpr char tgLinkKey[] = "tproxy_tg_link";
|
||||
constexpr char tmeLinkKey[] = "tproxy_tme_link";
|
||||
constexpr char isEnabledKey[] = "tproxy_is_enabled";
|
||||
constexpr char workersKey[] = "tproxy_workers";
|
||||
constexpr char httpPortKey[] = "tproxy_http_port";
|
||||
|
||||
constexpr char carrierModeHttps[] = "https";
|
||||
constexpr char carrierModeHttpsLanes[] = "https-lanes";
|
||||
constexpr char carrierModeWebsocket[] = "websocket";
|
||||
constexpr char carrierModeWebsocketLanes[] = "websocket-lanes";
|
||||
|
||||
constexpr char defaultHttpPort[] = "80";
|
||||
constexpr char defaultPort[] = "443";
|
||||
constexpr char defaultWorkers[] = "1";
|
||||
constexpr int maxWorkers = 32;
|
||||
}
|
||||
|
||||
} // namespace protocols
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace amnezia
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
TProxy,
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
|
||||
@@ -79,6 +79,7 @@ QMap<DockerContainer, QString> ContainerUtils::containerHumanNames()
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") },
|
||||
{ DockerContainer::MtProxy, QObject::tr("MTProxy (Telegram)") },
|
||||
{ DockerContainer::Telemt, QObject::tr("Telemt (Telegram)") },
|
||||
{ DockerContainer::TProxy, QObject::tr("TProxy (Telegram WEB)") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,6 +118,8 @@ QMap<DockerContainer, QString> ContainerUtils::containerDescriptions()
|
||||
QObject::tr("Telegram MTProto proxy server") },
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy (Telemt, Rust)") },
|
||||
{ DockerContainer::TProxy,
|
||||
QObject::tr("Telegram WEB proxy (tproxy-server)") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,6 +199,10 @@ QMap<DockerContainer, QString> ContainerUtils::containerDetailedDescriptions()
|
||||
{ DockerContainer::Telemt,
|
||||
QObject::tr("Telegram MTProto proxy powered by Telemt (Rust). "
|
||||
"Supports secure and TLS fronting modes with optional traffic masking.") },
|
||||
{ DockerContainer::TProxy,
|
||||
QObject::tr("Telegram WEB proxy. Clients connect over HTTPS to a hostname; "
|
||||
"the server relays traffic to official MTProxy. Requires a domain, "
|
||||
"ports 80 and 443, and a WEB-capable Telegram app.") },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,6 +234,7 @@ Proto ContainerUtils::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
|
||||
case DockerContainer::MtProxy: return Proto::MtProxy;
|
||||
case DockerContainer::Telemt: return Proto::Telemt;
|
||||
case DockerContainer::TProxy: return Proto::TProxy;
|
||||
default: return Proto::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -256,6 +264,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
case DockerContainer::TProxy: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -273,6 +282,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
case DockerContainer::TProxy: return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -293,6 +303,7 @@ bool ContainerUtils::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::SSXray: return true;
|
||||
case DockerContainer::MtProxy: return true;
|
||||
case DockerContainer::Telemt: return true;
|
||||
case DockerContainer::TProxy: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -361,6 +372,7 @@ bool ContainerUtils::isShareable(DockerContainer container)
|
||||
case DockerContainer::Socks5Proxy: return false;
|
||||
case DockerContainer::MtProxy: return false;
|
||||
case DockerContainer::Telemt: return false;
|
||||
case DockerContainer::TProxy: return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -396,6 +408,7 @@ int ContainerUtils::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::SSXray: return 8;
|
||||
case DockerContainer::MtProxy:
|
||||
case DockerContainer::Telemt:
|
||||
case DockerContainer::TProxy:
|
||||
return 20;
|
||||
default: return 0;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace amnezia
|
||||
Socks5Proxy,
|
||||
MtProxy,
|
||||
Telemt,
|
||||
TProxy,
|
||||
};
|
||||
Q_ENUM_NS(Proto)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/mtProxyProtocolConfig.h"
|
||||
#include "core/models/protocols/telemtProtocolConfig.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace ProtocolUtils;
|
||||
@@ -41,6 +42,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
|
||||
case DockerContainer::MtProxy: return QLatin1String("mtproxy");
|
||||
case DockerContainer::Telemt: return QLatin1String("telemt");
|
||||
case DockerContainer::TProxy: return QLatin1String("tproxy");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
@@ -402,6 +404,30 @@ amnezia::ScriptVars amnezia::genTelemtVars(const ContainerConfig &containerConfi
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genTProxyVars(const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
|
||||
if (auto *tProxyConfig = containerConfig.getTProxyProtocolConfig()) {
|
||||
const TProxyProtocolConfig &c = *tProxyConfig;
|
||||
vars.append({{"$TPROXY_SECRET", c.secret}});
|
||||
vars.append({{"$TPROXY_REGENERATE_SECRET",
|
||||
c.secret.isEmpty() ? QStringLiteral("1") : QStringLiteral("0")}});
|
||||
vars.append({{"$TPROXY_HOSTNAME", c.hostname}});
|
||||
vars.append({{"$TPROXY_ACME_EMAIL", c.acmeEmail}});
|
||||
vars.append({{"$TPROXY_HTTPS_PORT",
|
||||
c.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : c.port}});
|
||||
vars.append({{"$TPROXY_HTTP_PORT",
|
||||
c.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : c.httpPort}});
|
||||
vars.append({{"$TPROXY_CARRIER_MODE",
|
||||
c.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps) : c.carrierMode}});
|
||||
vars.append({{"$TPROXY_WORKERS",
|
||||
c.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : c.workers}});
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig)
|
||||
{
|
||||
ScriptVars vars;
|
||||
@@ -432,6 +458,9 @@ amnezia::ScriptVars amnezia::genProtocolVarsForContainer(DockerContainer contain
|
||||
case Proto::Telemt:
|
||||
vars.append(genTelemtVars(containerConfig));
|
||||
break;
|
||||
case Proto::TProxy:
|
||||
vars.append(genTProxyVars(containerConfig));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ ScriptVars genSftpVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genSocks5ProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genMtProxyVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genTelemtVars(const ContainerConfig &containerConfig);
|
||||
ScriptVars genTProxyVars(const ContainerConfig &containerConfig);
|
||||
|
||||
ScriptVars genProtocolVarsForContainer(DockerContainer container, const ContainerConfig &containerConfig);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ ErrorCode SshSession::runContainerScript(const ServerCredentials &credentials, D
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
const bool useSh = container == DockerContainer::Socks5Proxy || container == DockerContainer::MtProxy
|
||||
|| container == DockerContainer::Telemt || container == DockerContainer::TProxy;
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, useSh ? "sh" : "bash");
|
||||
e = runScript(credentials, replaceVars(runner, amnezia::genBaseVars(credentials, container, QString(), QString())), cbReadStdOut, cbReadStdErr);
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
<file>telemt/Dockerfile</file>
|
||||
<file>telemt/run_container.sh</file>
|
||||
<file>telemt/start.sh</file>
|
||||
<file>tproxy/configure_container.sh</file>
|
||||
<file>tproxy/Dockerfile</file>
|
||||
<file>tproxy/run_container.sh</file>
|
||||
<file>tproxy/start.sh</file>
|
||||
<file>openvpn/configure_container.sh</file>
|
||||
<file>openvpn/Dockerfile</file>
|
||||
<file>openvpn/run_container.sh</file>
|
||||
|
||||
@@ -10,6 +10,10 @@ sudo iptables -C FORWARD -i docker0 ! -o docker0 -j ACCEPT || sudo iptables -A F
|
||||
sudo iptables -C FORWARD -i docker0 -o docker0 -j ACCEPT || sudo iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT
|
||||
|
||||
# Tuning network
|
||||
# Telegram proxies (mtproxy/telemt/tproxy) each open hundreds of middle-proxy
|
||||
# connections; the default nf_conntrack_max (often 8192) overflows and new
|
||||
# connections get dropped, so raise it.
|
||||
sudo sysctl -w net.netfilter.nf_conntrack_max=262144 2>/dev/null || true; \
|
||||
sudo sysctl fs.file-max=51200; \
|
||||
sudo sysctl net.core.rmem_max=67108864; \
|
||||
sudo sysctl net.core.wmem_max=67108864; \
|
||||
|
||||
58
client/server_scripts/tproxy/Dockerfile
Normal file
58
client/server_scripts/tproxy/Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Telegram WEB proxy: Caddy + tproxy-server + official MTProxy.
|
||||
# Official MTProxy is x86_64-only (same constraint as upstream install.sh).
|
||||
|
||||
FROM golang:1.22-bookworm AS relay
|
||||
WORKDIR /src
|
||||
ARG TPROXY_COMMIT=2873a08806d6e4d84830b9b5c4b0ec0f46af91f8
|
||||
RUN set -eux; \
|
||||
apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o /tmp/tproxy.tar.gz \
|
||||
"https://github.com/telegramdesktop/tproxy-server/archive/${TPROXY_COMMIT}.tar.gz"; \
|
||||
tar -xzf /tmp/tproxy.tar.gz -C /src --strip-components=1; \
|
||||
go build -trimpath -o /out/tproxy-server ./cmd/tproxy-server
|
||||
|
||||
FROM debian:12-slim AS mtproxy
|
||||
ARG MTPROXY_COMMIT=f36d8af769ffaeac36978d38c2c0f6d1104c2137
|
||||
ARG MTPROXY_SHA256=919795c416b870670841a21d1930ad97a24c7b84b9eb8c6f9e3de32f2fdf4655
|
||||
RUN set -eux; \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl build-essential libssl-dev zlib1g-dev make && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o /tmp/MTProxy.tar.gz \
|
||||
"https://github.com/TelegramMessenger/MTProxy/archive/${MTPROXY_COMMIT}.tar.gz"; \
|
||||
echo "${MTPROXY_SHA256} /tmp/MTProxy.tar.gz" | sha256sum -c -; \
|
||||
mkdir -p /src && tar -xzf /tmp/MTProxy.tar.gz -C /src --strip-components=1; \
|
||||
make -C /src -j"$(nproc)"; \
|
||||
test -x /src/objs/bin/mtproto-proxy; \
|
||||
install -m 0755 /src/objs/bin/mtproto-proxy /mtproto-proxy
|
||||
|
||||
FROM debian:12-slim
|
||||
RUN set -eux; \
|
||||
ARCH="$(uname -m)"; \
|
||||
if [ "$ARCH" != "x86_64" ]; then \
|
||||
echo "tproxy-server requires an x86_64 server (official MTProxy is x86_64-only)" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl openssl libssl3 zlib1g && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
curl -fL --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 120 \
|
||||
-o /tmp/caddy.tgz \
|
||||
"https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_amd64.tar.gz"; \
|
||||
tar -xzf /tmp/caddy.tgz -C /usr/local/bin caddy; \
|
||||
chmod 0755 /usr/local/bin/caddy; \
|
||||
rm -f /tmp/caddy.tgz
|
||||
|
||||
COPY --from=relay /out/tproxy-server /usr/local/bin/tproxy-server
|
||||
COPY --from=mtproxy /mtproto-proxy /usr/local/bin/mtproto-proxy
|
||||
|
||||
RUN mkdir -p /opt/amnezia /data /data/site /data/caddy \
|
||||
&& printf '#!/bin/sh\ntail -f /dev/null\n' > /opt/amnezia/start.sh \
|
||||
&& chmod a+x /opt/amnezia/start.sh /usr/local/bin/tproxy-server /usr/local/bin/mtproto-proxy
|
||||
|
||||
VOLUME /data
|
||||
ENTRYPOINT ["/bin/sh", "/opt/amnezia/start.sh"]
|
||||
CMD [""]
|
||||
146
client/server_scripts/tproxy/configure_container.sh
Normal file
146
client/server_scripts/tproxy/configure_container.sh
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/bin/sh
|
||||
|
||||
mkdir -p /data/site /data/caddy
|
||||
|
||||
if [ "$TPROXY_REGENERATE_SECRET" = "1" ]; then
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
elif [ -n "$TPROXY_SECRET" ]; then
|
||||
SECRET="$TPROXY_SECRET"
|
||||
elif [ -f /data/secret ]; then
|
||||
SECRET=$(cat /data/secret)
|
||||
else
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
fi
|
||||
echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}$' || SECRET=$(openssl rand -hex 16)
|
||||
|
||||
HOST=$TPROXY_HOSTNAME
|
||||
EMAIL=$TPROXY_ACME_EMAIL
|
||||
CARRIER=$TPROXY_CARRIER_MODE
|
||||
[ -z "$CARRIER" ] && CARRIER=https
|
||||
WORKERS=$TPROXY_WORKERS
|
||||
[ -z "$WORKERS" ] && WORKERS=1
|
||||
HTTPS_PORT=$TPROXY_HTTPS_PORT
|
||||
[ -z "$HTTPS_PORT" ] && HTTPS_PORT=443
|
||||
HTTP_PORT=$TPROXY_HTTP_PORT
|
||||
[ -z "$HTTP_PORT" ] && HTTP_PORT=80
|
||||
|
||||
if [ -z "$HOST" ] || [ -z "$EMAIL" ]; then
|
||||
echo "[!] ERROR: TPROXY_HOSTNAME and TPROXY_ACME_EMAIL are required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$SECRET" > /data/secret
|
||||
chmod 600 /data/secret 2>/dev/null || true
|
||||
|
||||
{
|
||||
printf 'hostname=%s\n' "$HOST"
|
||||
printf 'email=%s\n' "$EMAIL"
|
||||
printf 'carrier=%s\n' "$CARRIER"
|
||||
printf 'workers=%s\n' "$WORKERS"
|
||||
printf 'http_port=%s\n' "$HTTP_PORT"
|
||||
printf 'https_port=%s\n' "$HTTPS_PORT"
|
||||
} > /data/tproxy-meta
|
||||
|
||||
# Decoy landing: unique enough per hostname/secret, not a shared Amnezia fingerprint page.
|
||||
SITE_ID=$(echo -n "${HOST}${SECRET}" | openssl dgst -sha256 | awk '{print substr($2,1,12)}')
|
||||
cat > /data/site/index.html << SITE
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${HOST}</title>
|
||||
<style>
|
||||
body { font-family: Georgia, serif; margin: 12% auto; max-width: 36em; color: #222; }
|
||||
h1 { font-weight: normal; font-size: 1.4em; }
|
||||
p { line-height: 1.5; color: #555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${HOST}</h1>
|
||||
<p>This host is reserved. Ref ${SITE_ID}.</p>
|
||||
</body>
|
||||
</html>
|
||||
SITE
|
||||
|
||||
cat > /data/config.json << EOF
|
||||
{
|
||||
"public_hostname": "$HOST",
|
||||
"listen": "127.0.0.1:8080",
|
||||
"admin_listen": "127.0.0.1:8081",
|
||||
"public_dir": "/data/site",
|
||||
"profiles_file": "/data/profiles.json",
|
||||
"enable_pprof": false
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > /data/profiles.json << EOF
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"name": "default",
|
||||
"secret": "$SECRET",
|
||||
"backend": "127.0.0.1:2398",
|
||||
"carrier_mode": "$CARRIER"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
chmod 0400 /data/profiles.json
|
||||
|
||||
curl -s --max-time 15 https://core.telegram.org/getProxySecret -o /data/proxy-secret
|
||||
if [ ! -s /data/proxy-secret ]; then
|
||||
echo "[!] WARNING: proxy-secret download failed or empty"
|
||||
fi
|
||||
curl -s --max-time 15 https://core.telegram.org/getProxyConfig -o /data/proxy-multi.conf
|
||||
if [ ! -s /data/proxy-multi.conf ]; then
|
||||
echo "[!] WARNING: proxy-multi.conf download failed or empty"
|
||||
fi
|
||||
|
||||
if [ -n "$EMAIL" ] && [ -n "$HOST" ]; then
|
||||
cat > /data/Caddyfile << EOF
|
||||
{
|
||||
email $EMAIL
|
||||
admin off
|
||||
servers {
|
||||
protocols h1 h2
|
||||
timeouts {
|
||||
read_header 10s
|
||||
read_body 60s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$HOST {
|
||||
reverse_proxy 127.0.0.1:8080 {
|
||||
transport http {
|
||||
read_timeout 40s
|
||||
write_timeout 40s
|
||||
}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-Host {host}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > /data/Caddyfile << EOF
|
||||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
:80 {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "[*] Secret: $SECRET"
|
||||
echo "[*] Hostname: $HOST"
|
||||
if [ -n "$HOST" ]; then
|
||||
echo "[*] tg:// link: tg://webproxy?server=${HOST}&secret=${SECRET}"
|
||||
echo "[*] t.me link: https://t.me/webproxy?server=${HOST}&secret=${SECRET}"
|
||||
else
|
||||
echo "[*] tg:// link: "
|
||||
echo "[*] t.me link: "
|
||||
fi
|
||||
9
client/server_scripts/tproxy/run_container.sh
Normal file
9
client/server_scripts/tproxy/run_container.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
# Run Telegram WEB proxy (host ports map to Caddy 80/443 inside the container)
|
||||
sudo docker run -d \
|
||||
--log-driver none \
|
||||
--restart always \
|
||||
-p $TPROXY_HTTP_PORT:80/tcp \
|
||||
-p $TPROXY_HTTPS_PORT:443/tcp \
|
||||
-v amnezia-tproxy-data:/data \
|
||||
--name $CONTAINER_NAME \
|
||||
$CONTAINER_NAME
|
||||
77
client/server_scripts/tproxy/start.sh
Normal file
77
client/server_scripts/tproxy/start.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! -f /data/secret ]; then
|
||||
echo "ERROR: /data/secret not found — run configure_container first"
|
||||
tail -f /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SECRET=$(cat /data/secret)
|
||||
WORKERS=1
|
||||
if [ -f /data/tproxy-meta ]; then
|
||||
_w=$(grep '^workers=' /data/tproxy-meta 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
[ -n "$_w" ] && WORKERS="$_w"
|
||||
fi
|
||||
|
||||
if [ ! -s /data/proxy-secret ]; then
|
||||
echo "[!] WARNING: /data/proxy-secret missing or empty"
|
||||
fi
|
||||
if [ ! -s /data/proxy-multi.conf ]; then
|
||||
echo "[!] WARNING: /data/proxy-multi.conf missing or empty"
|
||||
fi
|
||||
if [ ! -f /data/config.json ]; then
|
||||
echo "[!] WARNING: /data/config.json missing"
|
||||
fi
|
||||
if [ ! -f /data/Caddyfile ]; then
|
||||
echo "[!] WARNING: /data/Caddyfile missing"
|
||||
fi
|
||||
|
||||
mkdir -p /data/caddy /data/site
|
||||
export XDG_DATA_HOME=/data/caddy
|
||||
export XDG_CONFIG_HOME=/data/caddy
|
||||
|
||||
stop_children() {
|
||||
kill $MPID $RPID $CPID 2>/dev/null
|
||||
wait
|
||||
}
|
||||
trap 'stop_children; exit 0' TERM INT
|
||||
|
||||
# Official MTProxy runs inside docker (container IP != public IP). Without --nat-info
|
||||
# it fails middle-proxy registration and never opens upstream connections to Telegram
|
||||
# DC, so the relay gets no downstream bytes. Mirror the standalone mtproxy launch.
|
||||
NAT_FLAG=""
|
||||
NAT_VALUE=""
|
||||
INTERNAL_IP=$(hostname -i 2>/dev/null | awk '{print $1}')
|
||||
# Amnezia injects the resolved public server IP; api.ipify from inside the container
|
||||
# is unreliable (often empty) and an empty external IP breaks middle-proxy registration.
|
||||
EXTERNAL_IP="$SERVER_IP_ADDRESS"
|
||||
if [ -z "$EXTERNAL_IP" ]; then
|
||||
EXTERNAL_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null)
|
||||
[ -z "$EXTERNAL_IP" ] && EXTERNAL_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
fi
|
||||
if [ -n "$INTERNAL_IP" ] && [ -n "$EXTERNAL_IP" ] && [ "$INTERNAL_IP" != "$EXTERNAL_IP" ]; then
|
||||
NAT_FLAG="--nat-info"
|
||||
# mtproto-proxy wants a single arg: --nat-info <local-addr>:<global-addr> (colon-separated).
|
||||
NAT_VALUE="${INTERNAL_IP}:${EXTERNAL_IP}"
|
||||
fi
|
||||
|
||||
mtproto-proxy \
|
||||
-u root \
|
||||
-p 8888 \
|
||||
-H 2398 \
|
||||
-S ${SECRET} \
|
||||
--aes-pwd /data/proxy-secret \
|
||||
-M ${WORKERS} \
|
||||
-C 60000 \
|
||||
--allow-skip-dh \
|
||||
${NAT_FLAG:+${NAT_FLAG} ${NAT_VALUE}} \
|
||||
/data/proxy-multi.conf &
|
||||
MPID=$!
|
||||
|
||||
tproxy-server -config /data/config.json &
|
||||
RPID=$!
|
||||
|
||||
caddy run --config /data/Caddyfile --adapter caddyfile &
|
||||
CPID=$!
|
||||
|
||||
wait $MPID $RPID $CPID
|
||||
@@ -53,6 +53,7 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
Socks5ProxyConfigModel *socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel *telemtConfigModel,
|
||||
TProxyConfigModel *tProxyConfigModel,
|
||||
ConnectionController *connectionController,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -73,6 +74,7 @@ InstallUiController::InstallUiController(InstallController *installController,
|
||||
m_socks5ConfigModel(socks5ConfigModel),
|
||||
m_mtProxyConfigModel(mtConfigModel),
|
||||
m_telemtConfigModel(telemtConfigModel),
|
||||
m_tProxyConfigModel(tProxyConfigModel),
|
||||
m_connectionController(connectionController)
|
||||
{
|
||||
connect(m_installController, &InstallController::configValidated, this, &InstallUiController::configValidated);
|
||||
@@ -85,6 +87,11 @@ InstallUiController::~InstallUiController()
|
||||
|
||||
void InstallUiController::install(DockerContainer container, int port, TransportProto transportProto, const QString &serverId)
|
||||
{
|
||||
if (container == DockerContainer::TProxy && m_tProxyConfigModel) {
|
||||
m_installController->setTProxyInstallHints(m_tProxyConfigModel->getHostname(),
|
||||
m_tProxyConfigModel->getAcmeEmail());
|
||||
}
|
||||
|
||||
const bool isNewServer = serverId.isEmpty();
|
||||
|
||||
ServerCredentials serverCredentials;
|
||||
@@ -257,6 +264,10 @@ bool InstallUiController::buildContainerConfigFromModel(int containerIndex, int
|
||||
containerConfig.protocolConfig = m_telemtConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
case Proto::TProxy: {
|
||||
containerConfig.protocolConfig = m_tProxyConfigModel->getProtocolConfig();
|
||||
break;
|
||||
}
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: {
|
||||
containerConfig.protocolConfig = m_ikev2ConfigModel->getProtocolConfig();
|
||||
@@ -304,10 +315,12 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
ContainerConfig oldContainerConfig = m_serversController->getContainerConfig(serverId, container);
|
||||
|
||||
const bool asyncUpdate = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy
|
||||
|| container == DockerContainer::Xray || container == DockerContainer::SSXray;
|
||||
|
||||
if (asyncUpdate) {
|
||||
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt;
|
||||
const bool emitBusy = container == DockerContainer::MtProxy || container == DockerContainer::Telemt
|
||||
|| container == DockerContainer::TProxy;
|
||||
if (emitBusy)
|
||||
emit serverIsBusy(true);
|
||||
auto *watcher = new QFutureWatcher<ErrorCode>(this);
|
||||
@@ -358,7 +371,8 @@ void InstallUiController::updateServerConfig(const QString &serverId, int contai
|
||||
void InstallUiController::setContainerEnabled(const QString &serverId, int containerIndex, bool enabled)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
|
||||
&& container != DockerContainer::TProxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -389,7 +403,8 @@ void InstallUiController::setContainerEnabled(const QString &serverId, int conta
|
||||
void InstallUiController::refreshContainerStatus(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
|
||||
&& container != DockerContainer::TProxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,7 +460,8 @@ void InstallUiController::refreshContainerDiagnostics(const QString &serverId, i
|
||||
void InstallUiController::fetchContainerSecret(const QString &serverId, int containerIndex)
|
||||
{
|
||||
const DockerContainer container = static_cast<DockerContainer>(containerIndex);
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt) {
|
||||
if (container != DockerContainer::MtProxy && container != DockerContainer::Telemt
|
||||
&& container != DockerContainer::TProxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -711,6 +727,7 @@ void InstallUiController::updateProtocolConfigModel(const QString &serverId, int
|
||||
case Proto::Socks5Proxy: updateIfPresent(m_socks5ConfigModel, containerConfig.getSocks5ProxyProtocolConfig()); break;
|
||||
case Proto::MtProxy: updateIfPresent(m_mtProxyConfigModel, containerConfig.getMtProxyProtocolConfig()); break;
|
||||
case Proto::Telemt: updateIfPresent(m_telemtConfigModel, containerConfig.getTelemtProtocolConfig()); break;
|
||||
case Proto::TProxy: updateIfPresent(m_tProxyConfigModel, containerConfig.getTProxyProtocolConfig()); break;
|
||||
#ifdef Q_OS_WINDOWS
|
||||
case Proto::Ikev2: updateIfPresent(m_ikev2ConfigModel, containerConfig.getIkev2ProtocolConfig()); break;
|
||||
#endif
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
|
||||
#include "ui/models/services/mtProxyConfigModel.h"
|
||||
#include "ui/models/services/telemtConfigModel.h"
|
||||
#include "ui/models/services/tProxyConfigModel.h"
|
||||
|
||||
class InstallUiController : public QObject
|
||||
{
|
||||
@@ -53,6 +54,7 @@ public:
|
||||
Socks5ProxyConfigModel* socks5ConfigModel,
|
||||
MtProxyConfigModel* mtConfigModel,
|
||||
TelemtConfigModel* telemtConfigModel,
|
||||
TProxyConfigModel* tProxyConfigModel,
|
||||
ConnectionController* connectionController,
|
||||
QObject *parent = nullptr);
|
||||
~InstallUiController();
|
||||
@@ -155,6 +157,7 @@ private:
|
||||
Socks5ProxyConfigModel* m_socks5ConfigModel;
|
||||
MtProxyConfigModel* m_mtProxyConfigModel;
|
||||
TelemtConfigModel* m_telemtConfigModel;
|
||||
TProxyConfigModel* m_tProxyConfigModel;
|
||||
ConnectionController* m_connectionController;
|
||||
|
||||
ServerCredentials m_processedServerCredentials;
|
||||
|
||||
@@ -572,6 +572,8 @@ QStringList ServersUiController::getAllInstalledServicesName(int serverIndex) co
|
||||
servicesName.append("MTProxy");
|
||||
} else if (container == DockerContainer::Telemt) {
|
||||
servicesName.append("Telemt");
|
||||
} else if (container == DockerContainer::TProxy) {
|
||||
servicesName.append("TProxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
|
||||
case IsSocks5ProxyRole: return container == DockerContainer::Socks5Proxy;
|
||||
case IsMtProxyRole: return container == DockerContainer::MtProxy;
|
||||
case IsTelemtRole: return container == DockerContainer::Telemt;
|
||||
case IsTProxyRole: return container == DockerContainer::TProxy;
|
||||
case InstallPageOrderRole: return ContainerUtils::installPageOrder(container);
|
||||
}
|
||||
|
||||
@@ -191,5 +192,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
roles[IsTProxyRole] = "isTProxy";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
IsTProxyRole,
|
||||
};
|
||||
|
||||
Q_INVOKABLE void openContainerSettings(int containerIndex);
|
||||
|
||||
@@ -44,6 +44,7 @@ QHash<int, QByteArray> ProtocolsModel::roleNames() const
|
||||
roles[IsSocks5ProxyRole] = "isSocks5Proxy";
|
||||
roles[IsMtProxyRole] = "isMtProxy";
|
||||
roles[IsTelemtRole] = "isTelemt";
|
||||
roles[IsTProxyRole] = "isTProxy";
|
||||
|
||||
return roles;
|
||||
}
|
||||
@@ -75,6 +76,7 @@ QVariant ProtocolsModel::data(const QModelIndex &index, int role) const
|
||||
case IsSocks5ProxyRole: return proto == Proto::Socks5Proxy;
|
||||
case IsMtProxyRole: return proto == Proto::MtProxy;
|
||||
case IsTelemtRole: return proto == Proto::Telemt;
|
||||
case IsTProxyRole: return proto == Proto::TProxy;
|
||||
case RawConfigRole:
|
||||
return getRawConfig();
|
||||
case IsClientProtocolExistsRole:
|
||||
@@ -130,6 +132,7 @@ PageLoader::PageEnum ProtocolsModel::serverProtocolPage(Proto protocol) const
|
||||
case Proto::Socks5Proxy: return PageLoader::PageEnum::PageServiceSocksProxySettings;
|
||||
case Proto::MtProxy: return PageLoader::PageEnum::PageServiceMtProxySettings;
|
||||
case Proto::Telemt: return PageLoader::PageEnum::PageServiceTelemtSettings;
|
||||
case Proto::TProxy: return PageLoader::PageEnum::PageServiceTProxySettings;
|
||||
default: return PageLoader::PageEnum::PageProtocolOpenVpnSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
IsSocks5ProxyRole,
|
||||
IsMtProxyRole,
|
||||
IsTelemtRole,
|
||||
IsTProxyRole,
|
||||
};
|
||||
|
||||
explicit ProtocolsModel(QObject *parent = nullptr);
|
||||
|
||||
420
client/ui/models/services/tProxyConfigModel.cpp
Normal file
420
client/ui/models/services/tProxyConfigModel.cpp
Normal file
@@ -0,0 +1,420 @@
|
||||
#include "tProxyConfigModel.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/constants/protocolConstants.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace {
|
||||
const QRegularExpression kHostnameRe(QStringLiteral("^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$"));
|
||||
const QRegularExpression kEmailRe(QStringLiteral("^[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"));
|
||||
const QRegularExpression kHex32Re(QStringLiteral("^[0-9a-fA-F]{32}$"));
|
||||
}
|
||||
|
||||
TProxyConfigModel::TProxyConfigModel(QObject *parent) : QAbstractListModel(parent) {}
|
||||
|
||||
int TProxyConfigModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::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::HttpPortRole:
|
||||
m_protocolConfig.httpPort = value.toString();
|
||||
break;
|
||||
case Roles::SecretRole:
|
||||
m_protocolConfig.secret = value.toString();
|
||||
break;
|
||||
case Roles::HostnameRole: {
|
||||
const QString h = sanitizeHostnameFieldText(value.toString());
|
||||
if (!isValidHostname(h)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.hostname = h;
|
||||
break;
|
||||
}
|
||||
case Roles::AcmeEmailRole: {
|
||||
const QString e = sanitizeAcmeEmailFieldText(value.toString());
|
||||
if (!isValidAcmeEmail(e)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.acmeEmail = e;
|
||||
break;
|
||||
}
|
||||
case Roles::CarrierModeRole: {
|
||||
const QString m = value.toString();
|
||||
if (!isValidCarrierMode(m)) {
|
||||
return false;
|
||||
}
|
||||
m_protocolConfig.carrierMode = m;
|
||||
break;
|
||||
}
|
||||
case Roles::WorkersRole:
|
||||
m_protocolConfig.workers = sanitizeWorkersFieldText(value.toString());
|
||||
break;
|
||||
case Roles::IsEnabledRole:
|
||||
m_protocolConfig.isEnabled = value.toBool();
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
emit dataChanged(index, index, QList{role});
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariant TProxyConfigModel::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::tProxy::defaultPort) : m_protocolConfig.port;
|
||||
case Roles::HttpPortRole:
|
||||
return m_protocolConfig.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : m_protocolConfig.httpPort;
|
||||
case Roles::SecretRole:
|
||||
return m_protocolConfig.secret;
|
||||
case Roles::HostnameRole:
|
||||
return m_protocolConfig.hostname;
|
||||
case Roles::AcmeEmailRole:
|
||||
return m_protocolConfig.acmeEmail;
|
||||
case Roles::CarrierModeRole:
|
||||
return m_protocolConfig.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps)
|
||||
: m_protocolConfig.carrierMode;
|
||||
case Roles::WorkersRole:
|
||||
return m_protocolConfig.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers)
|
||||
: m_protocolConfig.workers;
|
||||
case Roles::TgLinkRole:
|
||||
return m_protocolConfig.tgLink;
|
||||
case Roles::TmeLinkRole:
|
||||
return m_protocolConfig.tmeLink;
|
||||
case Roles::IsEnabledRole:
|
||||
return m_protocolConfig.isEnabled;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void TProxyConfigModel::updateModel(DockerContainer container, const TProxyProtocolConfig &protocolConfig)
|
||||
{
|
||||
beginResetModel();
|
||||
m_container = container;
|
||||
m_protocolConfig = protocolConfig;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void TProxyConfigModel::updateModel(const QJsonObject &config)
|
||||
{
|
||||
beginResetModel();
|
||||
m_fullConfig = config;
|
||||
m_protocolConfig = TProxyProtocolConfig::fromJson(config.value(configKey::tproxy).toObject());
|
||||
if (m_protocolConfig.port.isEmpty()) {
|
||||
m_protocolConfig.port = protocols::tProxy::defaultPort;
|
||||
}
|
||||
if (m_protocolConfig.httpPort.isEmpty()) {
|
||||
m_protocolConfig.httpPort = protocols::tProxy::defaultHttpPort;
|
||||
}
|
||||
if (m_protocolConfig.carrierMode.isEmpty()) {
|
||||
m_protocolConfig.carrierMode = protocols::tProxy::carrierModeHttps;
|
||||
}
|
||||
if (m_protocolConfig.workers.isEmpty()) {
|
||||
m_protocolConfig.workers = protocols::tProxy::defaultWorkers;
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QJsonObject TProxyConfigModel::getConfig()
|
||||
{
|
||||
m_fullConfig.insert(configKey::tproxy, m_protocolConfig.toJson());
|
||||
return m_fullConfig;
|
||||
}
|
||||
|
||||
TProxyProtocolConfig TProxyConfigModel::getProtocolConfig()
|
||||
{
|
||||
return m_protocolConfig;
|
||||
}
|
||||
|
||||
void TProxyConfigModel::generateSecret()
|
||||
{
|
||||
QString secret;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
const quint32 byte = QRandomGenerator::global()->bounded(256);
|
||||
secret += QStringLiteral("%1").arg(byte, 2, 16, QChar('0'));
|
||||
}
|
||||
m_protocolConfig.secret = secret;
|
||||
emit dataChanged(index(0), index(0), QList<int>{SecretRole});
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setSecret(const QString &secret)
|
||||
{
|
||||
if (secret.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setData(index(0), secret, SecretRole);
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::validateAndSetSecret(const QString &rawSecret)
|
||||
{
|
||||
if (!kHex32Re.match(rawSecret).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
return setData(index(0), rawSecret, SecretRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setPort(const QString &port)
|
||||
{
|
||||
setData(index(0), sanitizePortFieldText(port), PortRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setHttpPort(const QString &port)
|
||||
{
|
||||
setData(index(0), sanitizePortFieldText(port), HttpPortRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setHostname(const QString &hostname)
|
||||
{
|
||||
setData(index(0), hostname, HostnameRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setAcmeEmail(const QString &email)
|
||||
{
|
||||
setData(index(0), email, AcmeEmailRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setCarrierMode(const QString &mode)
|
||||
{
|
||||
setData(index(0), mode, CarrierModeRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setWorkers(const QString &workers)
|
||||
{
|
||||
setData(index(0), workers, WorkersRole);
|
||||
}
|
||||
|
||||
void TProxyConfigModel::setEnabled(bool enabled)
|
||||
{
|
||||
setData(index(0), enabled, IsEnabledRole);
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getHostname() const
|
||||
{
|
||||
return m_protocolConfig.hostname;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getSecret() const
|
||||
{
|
||||
return m_protocolConfig.secret;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getAcmeEmail() const
|
||||
{
|
||||
return m_protocolConfig.acmeEmail;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getCarrierMode() const
|
||||
{
|
||||
return m_protocolConfig.carrierMode.isEmpty() ? QString(protocols::tProxy::carrierModeHttps)
|
||||
: m_protocolConfig.carrierMode;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getWorkers() const
|
||||
{
|
||||
return m_protocolConfig.workers.isEmpty() ? QString(protocols::tProxy::defaultWorkers) : m_protocolConfig.workers;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getPort() const
|
||||
{
|
||||
return m_protocolConfig.port.isEmpty() ? QString(protocols::tProxy::defaultPort) : m_protocolConfig.port;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::getHttpPort() const
|
||||
{
|
||||
return m_protocolConfig.httpPort.isEmpty() ? QString(protocols::tProxy::defaultHttpPort) : m_protocolConfig.httpPort;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::defaultPort() const
|
||||
{
|
||||
return QString(protocols::tProxy::defaultPort);
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::defaultHttpPort() const
|
||||
{
|
||||
return QString(protocols::tProxy::defaultHttpPort);
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::defaultWorkers() const
|
||||
{
|
||||
return QString(protocols::tProxy::defaultWorkers);
|
||||
}
|
||||
|
||||
int TProxyConfigModel::maxWorkers() const
|
||||
{
|
||||
return protocols::tProxy::maxWorkers;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::carrierModeHttps() const
|
||||
{
|
||||
return QString(protocols::tProxy::carrierModeHttps);
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::isValidHostname(const QString &host) const
|
||||
{
|
||||
if (host.isEmpty() || !host.contains(QLatin1Char('.'))) {
|
||||
return false;
|
||||
}
|
||||
return kHostnameRe.match(host).hasMatch();
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::isHostnameTypingIncomplete(const QString &text) const
|
||||
{
|
||||
const QString t = text.trimmed().toLower();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.startsWith(QLatin1Char('-')) || t.startsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char('.')) || t.endsWith(QLatin1Char('-'))) {
|
||||
return true;
|
||||
}
|
||||
return !t.contains(QLatin1Char('.'));
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::sanitizeHostnameFieldText(const QString &input) const
|
||||
{
|
||||
QString out = input.trimmed().toLower();
|
||||
out.remove(QRegularExpression(QStringLiteral("[^a-z0-9.-]")));
|
||||
while (out.startsWith(QLatin1Char('-')) || out.startsWith(QLatin1Char('.'))) {
|
||||
out.remove(0, 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::isValidAcmeEmail(const QString &email) const
|
||||
{
|
||||
return kEmailRe.match(email.trimmed()).hasMatch();
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::isAcmeEmailTypingIncomplete(const QString &text) const
|
||||
{
|
||||
const QString t = text.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (t.endsWith(QLatin1Char('.')) || t.endsWith(QLatin1Char('@')) || t.endsWith(QLatin1Char('-'))
|
||||
|| t.endsWith(QLatin1Char('+')) || t.endsWith(QLatin1Char('_'))) {
|
||||
return true;
|
||||
}
|
||||
if (!t.contains(QLatin1Char('@'))) {
|
||||
return true;
|
||||
}
|
||||
const int at = t.indexOf(QLatin1Char('@'));
|
||||
const QString domain = t.mid(at + 1);
|
||||
if (!domain.contains(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::sanitizeAcmeEmailFieldText(const QString &input) const
|
||||
{
|
||||
QString out;
|
||||
for (const QChar c : input.trimmed()) {
|
||||
if (c.isLetterOrNumber() || c == QLatin1Char('.') || c == QLatin1Char('_') || c == QLatin1Char('+')
|
||||
|| c == QLatin1Char('-') || c == QLatin1Char('@')) {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
const int firstAt = out.indexOf(QLatin1Char('@'));
|
||||
if (firstAt >= 0) {
|
||||
const int secondAt = out.indexOf(QLatin1Char('@'), firstAt + 1);
|
||||
if (secondAt >= 0) {
|
||||
out.remove(secondAt, 1);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool TProxyConfigModel::isValidCarrierMode(const QString &mode) const
|
||||
{
|
||||
return mode == QLatin1String(protocols::tProxy::carrierModeHttps)
|
||||
|| mode == QLatin1String(protocols::tProxy::carrierModeHttpsLanes)
|
||||
|| mode == QLatin1String(protocols::tProxy::carrierModeWebsocket)
|
||||
|| mode == QLatin1String(protocols::tProxy::carrierModeWebsocketLanes);
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::carrierModeLabel(const QString &mode) const
|
||||
{
|
||||
if (mode == QLatin1String(protocols::tProxy::carrierModeHttpsLanes)) {
|
||||
return QStringLiteral("HTTPS lanes");
|
||||
}
|
||||
if (mode == QLatin1String(protocols::tProxy::carrierModeWebsocket)) {
|
||||
return QStringLiteral("WebSocket");
|
||||
}
|
||||
if (mode == QLatin1String(protocols::tProxy::carrierModeWebsocketLanes)) {
|
||||
return QStringLiteral("WebSocket lanes");
|
||||
}
|
||||
return QStringLiteral("HTTPS");
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::sanitizeWorkersFieldText(const QString &input) const
|
||||
{
|
||||
QString digits;
|
||||
for (const QChar c : input) {
|
||||
if (c.isDigit()) {
|
||||
digits.append(c);
|
||||
}
|
||||
}
|
||||
bool ok = false;
|
||||
const int n = digits.toInt(&ok);
|
||||
if (!ok || n < 1) {
|
||||
return QString(protocols::tProxy::defaultWorkers);
|
||||
}
|
||||
if (n > protocols::tProxy::maxWorkers) {
|
||||
return QString::number(protocols::tProxy::maxWorkers);
|
||||
}
|
||||
return QString::number(n);
|
||||
}
|
||||
|
||||
QString TProxyConfigModel::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;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TProxyConfigModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[PortRole] = "port";
|
||||
roles[HttpPortRole] = "httpPort";
|
||||
roles[SecretRole] = "secret";
|
||||
roles[HostnameRole] = "hostname";
|
||||
roles[AcmeEmailRole] = "acmeEmail";
|
||||
roles[CarrierModeRole] = "carrierMode";
|
||||
roles[WorkersRole] = "workers";
|
||||
roles[TgLinkRole] = "tgLink";
|
||||
roles[TmeLinkRole] = "tmeLink";
|
||||
roles[IsEnabledRole] = "isEnabled";
|
||||
return roles;
|
||||
}
|
||||
87
client/ui/models/services/tProxyConfigModel.h
Normal file
87
client/ui/models/services/tProxyConfigModel.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#ifndef TPROXYCONFIGMODEL_H
|
||||
#define TPROXYCONFIGMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonObject>
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/models/protocols/tProxyProtocolConfig.h"
|
||||
|
||||
class TProxyConfigModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
PortRole = Qt::UserRole + 1,
|
||||
HttpPortRole,
|
||||
SecretRole,
|
||||
HostnameRole,
|
||||
AcmeEmailRole,
|
||||
CarrierModeRole,
|
||||
WorkersRole,
|
||||
TgLinkRole,
|
||||
TmeLinkRole,
|
||||
IsEnabledRole
|
||||
};
|
||||
|
||||
explicit TProxyConfigModel(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::TProxyProtocolConfig &protocolConfig);
|
||||
void updateModel(const QJsonObject &config);
|
||||
|
||||
QJsonObject getConfig();
|
||||
amnezia::TProxyProtocolConfig 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 setHttpPort(const QString &port);
|
||||
Q_INVOKABLE void setHostname(const QString &hostname);
|
||||
Q_INVOKABLE void setAcmeEmail(const QString &email);
|
||||
Q_INVOKABLE void setCarrierMode(const QString &mode);
|
||||
Q_INVOKABLE void setWorkers(const QString &workers);
|
||||
Q_INVOKABLE void setEnabled(bool enabled);
|
||||
|
||||
Q_INVOKABLE QString getHostname() const;
|
||||
Q_INVOKABLE QString getSecret() const;
|
||||
Q_INVOKABLE QString getAcmeEmail() const;
|
||||
Q_INVOKABLE QString getCarrierMode() const;
|
||||
Q_INVOKABLE QString getWorkers() const;
|
||||
Q_INVOKABLE QString getPort() const;
|
||||
Q_INVOKABLE QString getHttpPort() const;
|
||||
|
||||
Q_INVOKABLE QString defaultPort() const;
|
||||
Q_INVOKABLE QString defaultHttpPort() const;
|
||||
Q_INVOKABLE QString defaultWorkers() const;
|
||||
Q_INVOKABLE int maxWorkers() const;
|
||||
Q_INVOKABLE QString carrierModeHttps() const;
|
||||
|
||||
Q_INVOKABLE bool isValidHostname(const QString &host) const;
|
||||
Q_INVOKABLE bool isHostnameTypingIncomplete(const QString &text) const;
|
||||
Q_INVOKABLE QString sanitizeHostnameFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isValidAcmeEmail(const QString &email) const;
|
||||
Q_INVOKABLE bool isAcmeEmailTypingIncomplete(const QString &text) const;
|
||||
Q_INVOKABLE QString sanitizeAcmeEmailFieldText(const QString &input) const;
|
||||
|
||||
Q_INVOKABLE bool isValidCarrierMode(const QString &mode) const;
|
||||
Q_INVOKABLE QString carrierModeLabel(const QString &mode) const;
|
||||
|
||||
Q_INVOKABLE QString sanitizeWorkersFieldText(const QString &input) const;
|
||||
Q_INVOKABLE QString sanitizePortFieldText(const QString &input) const;
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
amnezia::DockerContainer m_container;
|
||||
QJsonObject m_fullConfig;
|
||||
amnezia::TProxyProtocolConfig m_protocolConfig;
|
||||
};
|
||||
|
||||
#endif // TPROXYCONFIGMODEL_H
|
||||
@@ -54,6 +54,9 @@ ListViewType {
|
||||
} else if (isTelemt) {
|
||||
TelemtConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceTelemtSettings, false)
|
||||
} else if (isTProxy) {
|
||||
TProxyConfigModel.updateModel(config)
|
||||
PageController.goToPage(PageEnum.PageServiceTProxySettings, false)
|
||||
} else {
|
||||
InstallController.updateProtocols(ServersUiController.processedServerId, containerIndex)
|
||||
PageController.goToPage(PageEnum.PageSettingsServerProtocol)
|
||||
|
||||
963
client/ui/qml/Pages2/PageServiceTProxySettings.qml
Normal file
963
client/ui/qml/Pages2/PageServiceTProxySettings.qml
Normal file
@@ -0,0 +1,963 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property int containerStatus: 1
|
||||
property int statusErrorCode: 0
|
||||
property bool isUpdating: false
|
||||
property bool isCheckingStatus: false
|
||||
property bool isFetchingSecret: false
|
||||
property bool previousEnabled: true
|
||||
property int previousContainerStatus: 1
|
||||
property string previousHostname: ""
|
||||
property string previousEmail: ""
|
||||
property string previousSecret: ""
|
||||
property string savedHostname: ""
|
||||
property string savedCarrierMode: TProxyConfigModel.carrierModeHttps()
|
||||
property string savedWorkers: TProxyConfigModel.defaultWorkers()
|
||||
property bool pendingUpdateAfterEnable: false
|
||||
property bool remoteOperationBusy: false
|
||||
|
||||
readonly property bool tproxyNetworkBlocked: !NetworkReachabilityController.hasInternetAccess
|
||||
readonly property bool pageBusy: isCheckingStatus || isFetchingSecret || isUpdating || remoteOperationBusy
|
||||
readonly property bool navigationBlockedWhileBusy: pageBusy
|
||||
|
||||
property bool pageOpenHandled: false
|
||||
property bool busyIndicatorShown: false
|
||||
property bool containerStatusRefreshCallPending: false
|
||||
|
||||
function syncPageBusyIndicator() {
|
||||
if (!root.pageOpenHandled) {
|
||||
return
|
||||
}
|
||||
var wantBusy = root.pageBusy
|
||||
if (wantBusy === root.busyIndicatorShown) {
|
||||
return
|
||||
}
|
||||
root.busyIndicatorShown = wantBusy
|
||||
PageController.showBusyIndicator(wantBusy)
|
||||
}
|
||||
|
||||
onPageBusyChanged: syncPageBusyIndicator()
|
||||
|
||||
function tproxyRequestContainerStatusRefresh() {
|
||||
if (!NetworkReachabilityController.hasInternetAccess) {
|
||||
isCheckingStatus = false
|
||||
syncPageBusyIndicator()
|
||||
return
|
||||
}
|
||||
isCheckingStatus = true
|
||||
syncPageBusyIndicator()
|
||||
InstallController.refreshContainerStatus(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex)
|
||||
}
|
||||
|
||||
function tproxyScheduleContainerStatusRefresh() {
|
||||
if (containerStatusRefreshCallPending) {
|
||||
return
|
||||
}
|
||||
containerStatusRefreshCallPending = true
|
||||
Qt.callLater(function () {
|
||||
containerStatusRefreshCallPending = false
|
||||
root.tproxyRequestContainerStatusRefresh()
|
||||
})
|
||||
}
|
||||
|
||||
function tproxyOnPageShown() {
|
||||
if (root.pageOpenHandled) {
|
||||
return
|
||||
}
|
||||
root.pageOpenHandled = true
|
||||
|
||||
PageController.disableControls(navigationBlockedWhileBusy)
|
||||
|
||||
if (!NetworkReachabilityController.hasInternetAccess) {
|
||||
isCheckingStatus = false
|
||||
} else {
|
||||
isCheckingStatus = true
|
||||
}
|
||||
syncPageBusyIndicator()
|
||||
root.tproxyScheduleContainerStatusRefresh()
|
||||
}
|
||||
|
||||
function scheduleUpdate() {
|
||||
Qt.callLater(function () {
|
||||
InstallController.updateServerConfig(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex, ProtocolEnum.TProxy, false)
|
||||
})
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onUpdateContainerFinished(message, closePage) {
|
||||
if (!root.visible) {
|
||||
isUpdating = false
|
||||
isCheckingStatus = false
|
||||
isFetchingSecret = false
|
||||
return
|
||||
}
|
||||
isUpdating = false
|
||||
containerStatus = 1
|
||||
savedHostname = TProxyConfigModel.getHostname()
|
||||
savedCarrierMode = TProxyConfigModel.getCarrierMode()
|
||||
savedWorkers = TProxyConfigModel.getWorkers()
|
||||
PageController.showNotificationMessage(message)
|
||||
}
|
||||
function onInstallationErrorOccurred() {
|
||||
if (!root.visible) {
|
||||
isUpdating = false
|
||||
isCheckingStatus = false
|
||||
isFetchingSecret = false
|
||||
return
|
||||
}
|
||||
isUpdating = false
|
||||
isFetchingSecret = false
|
||||
containerStatus = previousContainerStatus
|
||||
TProxyConfigModel.setEnabled(previousEnabled)
|
||||
TProxyConfigModel.setHostname(previousHostname)
|
||||
TProxyConfigModel.setAcmeEmail(previousEmail)
|
||||
TProxyConfigModel.setCarrierMode(savedCarrierMode)
|
||||
TProxyConfigModel.setWorkers(savedWorkers)
|
||||
if (previousSecret !== "")
|
||||
TProxyConfigModel.setSecret(previousSecret)
|
||||
}
|
||||
function onSetContainerEnabledFinished(enabled) {
|
||||
if (!root.visible) {
|
||||
isUpdating = false
|
||||
return
|
||||
}
|
||||
if (enabled && root.pendingUpdateAfterEnable) {
|
||||
root.pendingUpdateAfterEnable = false
|
||||
isUpdating = true
|
||||
root.scheduleUpdate()
|
||||
return
|
||||
}
|
||||
isUpdating = false
|
||||
containerStatus = enabled ? 1 : 2
|
||||
PageController.showNotificationMessage(enabled ? qsTr("TProxy started") : qsTr("TProxy stopped"))
|
||||
}
|
||||
function onServerIsBusy(busy) {
|
||||
root.remoteOperationBusy = busy
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
savedHostname = TProxyConfigModel.getHostname()
|
||||
savedCarrierMode = TProxyConfigModel.getCarrierMode()
|
||||
savedWorkers = TProxyConfigModel.getWorkers()
|
||||
if (status === 1) {
|
||||
TProxyConfigModel.setEnabled(true)
|
||||
isFetchingSecret = true
|
||||
isCheckingStatus = false
|
||||
InstallController.fetchContainerSecret(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex)
|
||||
} else {
|
||||
isFetchingSecret = false
|
||||
isCheckingStatus = false
|
||||
if (status === 2) {
|
||||
TProxyConfigModel.setEnabled(false)
|
||||
}
|
||||
}
|
||||
syncPageBusyIndicator()
|
||||
}
|
||||
function onContainerSecretFetched(value) {
|
||||
if (!root.visible) {
|
||||
isFetchingSecret = false
|
||||
return
|
||||
}
|
||||
isFetchingSecret = false
|
||||
syncPageBusyIndicator()
|
||||
if (value !== "")
|
||||
TProxyConfigModel.validateAndSetSecret(value)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
savedHostname = TProxyConfigModel.getHostname()
|
||||
savedCarrierMode = TProxyConfigModel.getCarrierMode()
|
||||
savedWorkers = TProxyConfigModel.getWorkers()
|
||||
Qt.callLater(root.tproxyOnPageShown)
|
||||
}
|
||||
|
||||
onNavigationBlockedWhileBusyChanged: {
|
||||
if (root.visible) {
|
||||
PageController.disableControls(navigationBlockedWhileBusy)
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
root.pageOpenHandled = false
|
||||
containerStatusRefreshCallPending = false
|
||||
isCheckingStatus = false
|
||||
isFetchingSecret = false
|
||||
busyIndicatorShown = false
|
||||
statusErrorCode = 0
|
||||
PageController.disableControls(false)
|
||||
PageController.showBusyIndicator(false)
|
||||
} else {
|
||||
root.tproxyOnPageShown()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NetworkReachabilityController
|
||||
|
||||
function onHasInternetAccessChanged() {
|
||||
if (!root.visible) {
|
||||
return
|
||||
}
|
||||
if (NetworkReachabilityController.hasInternetAccess) {
|
||||
root.tproxyScheduleContainerStatusRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentLayer
|
||||
anchors.fill: parent
|
||||
enabled: !root.pageBusy
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + PageController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
if (mainTabBar.currentIndex === 0) {
|
||||
connectionListView.positionViewAtBeginning()
|
||||
} else {
|
||||
settingsListView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: pageHeader
|
||||
anchors.top: backButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 24
|
||||
headerText: qsTr("TProxy settings")
|
||||
descriptionLinkText: qsTr("Read more about Telegram WEB proxy")
|
||||
descriptionLinkUrl: "https://github.com/telegramdesktop/tproxy-server"
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
visible: root.tproxyNetworkBlocked
|
||||
text: qsTr("No internet connection. Connect to the internet to change TProxy settings.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: mainTabBar
|
||||
anchors.top: pageHeader.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
width: parent.width
|
||||
background: Rectangle {
|
||||
color: AmneziaStyle.color.transparent
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
anchors.bottom: parent.bottom
|
||||
color: AmneziaStyle.color.slateGray
|
||||
}
|
||||
}
|
||||
TabButtonType {
|
||||
text: qsTr("Connection")
|
||||
isSelected: mainTabBar.currentIndex === 0
|
||||
}
|
||||
TabButtonType {
|
||||
text: qsTr("Settings")
|
||||
isSelected: mainTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
anchors.top: mainTabBar.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
currentIndex: mainTabBar.currentIndex
|
||||
|
||||
ListViewType {
|
||||
id: connectionListView
|
||||
model: TProxyConfigModel
|
||||
delegate: ColumnLayout {
|
||||
width: connectionListView.width
|
||||
spacing: 0
|
||||
|
||||
function effectiveHost() {
|
||||
return root.savedHostname !== "" ? root.savedHostname : hostname
|
||||
}
|
||||
|
||||
function displayTmeLink() {
|
||||
if (tmeLink !== "")
|
||||
return tmeLink
|
||||
var host = effectiveHost()
|
||||
if (host === "" || secret === "")
|
||||
return ""
|
||||
return "https://t.me/webproxy?server=" + host + "&secret=" + secret
|
||||
}
|
||||
|
||||
function displayTgLink() {
|
||||
if (tgLink !== "")
|
||||
return tgLink
|
||||
var host = effectiveHost()
|
||||
if (host === "" || secret === "")
|
||||
return ""
|
||||
return "tg://webproxy?server=" + host + "&secret=" + secret
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
text: qsTr("Use Telegram WEB proxy link")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
implicitHeight: linkRow.implicitHeight + 16
|
||||
color: AmneziaStyle.color.onyxBlack
|
||||
radius: 8
|
||||
border.color: AmneziaStyle.color.slateGray
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: linkRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 8
|
||||
spacing: 4
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
text: secret !== "" ? displayTmeLink() : qsTr("Set a hostname first")
|
||||
color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/copy.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
visible: secret !== ""
|
||||
onClicked: {
|
||||
GC.copyToClipBoard(displayTmeLink())
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
implicitHeight: tgLinkRow.implicitHeight + 16
|
||||
color: AmneziaStyle.color.onyxBlack
|
||||
radius: 8
|
||||
border.color: AmneziaStyle.color.slateGray
|
||||
border.width: 1
|
||||
visible: secret !== ""
|
||||
|
||||
RowLayout {
|
||||
id: tgLinkRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 8
|
||||
spacing: 4
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
text: displayTgLink()
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/qr-code.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
ExportController.generateQrFromStringRaw(displayTgLink())
|
||||
PageController.goToShareConnectionPage(qsTr("Telegram connection link"),
|
||||
qsTr("TProxy WEB proxy link"), "", "", "")
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/copy.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
GC.copyToClipBoard(displayTgLink())
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
spacing: 4
|
||||
|
||||
CaptionTextType {
|
||||
text: qsTr("Or enter the proxy details manually.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("How to do it")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Qt.openUrlExternally("https://github.com/telegramdesktop/tproxy-server")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
implicitHeight: manualCol.implicitHeight + 8
|
||||
color: AmneziaStyle.color.onyxBlack
|
||||
radius: 8
|
||||
border.color: AmneziaStyle.color.slateGray
|
||||
border.width: 1
|
||||
visible: secret !== ""
|
||||
|
||||
ColumnLayout {
|
||||
id: manualCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 8
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 12
|
||||
Layout.rightMargin: 8
|
||||
Layout.bottomMargin: 8
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
CaptionTextType {
|
||||
text: qsTr("Host")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
}
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
text: effectiveHost()
|
||||
color: AmneziaStyle.color.paleGray
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/copy.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
GC.copyToClipBoard(effectiveHost())
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 12
|
||||
Layout.rightMargin: 8
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 8
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
CaptionTextType {
|
||||
text: qsTr("Secret")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
}
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
text: secret
|
||||
color: AmneziaStyle.color.paleGray
|
||||
wrapMode: Text.WrapAnywhere
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
ImageButtonType {
|
||||
implicitWidth: 36
|
||||
implicitHeight: 36
|
||||
hoverEnabled: true
|
||||
image: "qrc:/images/controls/copy.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
onClicked: {
|
||||
GC.copyToClipBoard(secret)
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: 16
|
||||
text: qsTr("Needs a WEB-capable Telegram client (Desktop proof of concept). Classic MTProxy links will not work.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: removeButton
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 24
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 16
|
||||
visible: ServersUiController.isProcessedServerHasWriteAccess()
|
||||
text: qsTr("Delete TProxy")
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
clickedFunction: function () {
|
||||
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
|
||||
var yesButtonFunction = function () {
|
||||
PageController.goToPage(PageEnum.PageDeinstalling)
|
||||
InstallController.removeContainer(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex)
|
||||
}
|
||||
showQuestionDrawer(headerText,
|
||||
qsTr("The proxy will be stopped and all users will lose access."),
|
||||
qsTr("Continue"), qsTr("Cancel"), yesButtonFunction, function () {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: settingsListView
|
||||
model: TProxyConfigModel
|
||||
delegate: ColumnLayout {
|
||||
width: settingsListView.width
|
||||
spacing: 0
|
||||
readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy
|
||||
|
||||
SwitcherType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
text: qsTr("Enable TProxy")
|
||||
checked: isEnabled
|
||||
enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy && !root.tproxyNetworkBlocked
|
||||
onToggled: function () {
|
||||
if (checked !== isEnabled) {
|
||||
previousEnabled = isEnabled
|
||||
previousContainerStatus = containerStatus
|
||||
root.previousSecret = secret
|
||||
isEnabled = checked
|
||||
isUpdating = true
|
||||
if (checked) {
|
||||
root.pendingUpdateAfterEnable = true
|
||||
InstallController.setContainerEnabled(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex, true)
|
||||
} else {
|
||||
InstallController.setContainerEnabled(ServersUiController.processedServerId,
|
||||
ServersUiController.processedContainerIndex, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
visible: !fieldsEditable && !root.pageBusy
|
||||
text: (containerStatus === 1 || containerStatus === 2)
|
||||
? qsTr("Enable TProxy 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
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: hostnameTextField
|
||||
enabled: fieldsEditable
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
headerText: qsTr("Hostname")
|
||||
textField.placeholderText: qsTr("proxy.example.com")
|
||||
textField.text: hostname
|
||||
textField.onTextChanged: {
|
||||
var cur = hostnameTextField.textField.text
|
||||
var clean = TProxyConfigModel.sanitizeHostnameFieldText(cur)
|
||||
if (clean !== cur) {
|
||||
textField.text = clean
|
||||
textField.cursorPosition = clean.length
|
||||
return
|
||||
}
|
||||
if (TProxyConfigModel.isHostnameTypingIncomplete(cur)) {
|
||||
hostnameTextField.errorText = ""
|
||||
return
|
||||
}
|
||||
if (!TProxyConfigModel.isValidHostname(clean)) {
|
||||
hostnameTextField.errorText = qsTr("Use lowercase letters, digits, dots and hyphens")
|
||||
return
|
||||
}
|
||||
hostnameTextField.errorText = ""
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
var h = TProxyConfigModel.sanitizeHostnameFieldText(textField.text)
|
||||
textField.text = h
|
||||
if (!TProxyConfigModel.isValidHostname(h)) {
|
||||
hostnameTextField.errorText = qsTr("Enter a lowercase DNS hostname (A record to this server)")
|
||||
return
|
||||
}
|
||||
hostnameTextField.errorText = ""
|
||||
TProxyConfigModel.setHostname(h)
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
text: qsTr("Required. Point a DNS A record at this server. Do not put a CDN in front.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: emailTextField
|
||||
enabled: fieldsEditable
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
headerText: qsTr("ACME email")
|
||||
textField.placeholderText: qsTr("you@example.com")
|
||||
textField.text: acmeEmail
|
||||
textField.onTextChanged: {
|
||||
var cur = emailTextField.textField.text
|
||||
var clean = TProxyConfigModel.sanitizeAcmeEmailFieldText(cur)
|
||||
if (clean !== cur) {
|
||||
textField.text = clean
|
||||
textField.cursorPosition = clean.length
|
||||
return
|
||||
}
|
||||
if (TProxyConfigModel.isAcmeEmailTypingIncomplete(cur)) {
|
||||
emailTextField.errorText = ""
|
||||
return
|
||||
}
|
||||
if (!TProxyConfigModel.isValidAcmeEmail(clean)) {
|
||||
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
|
||||
return
|
||||
}
|
||||
emailTextField.errorText = ""
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
var e = TProxyConfigModel.sanitizeAcmeEmailFieldText(textField.text)
|
||||
textField.text = e
|
||||
if (!TProxyConfigModel.isValidAcmeEmail(e)) {
|
||||
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
|
||||
return
|
||||
}
|
||||
emailTextField.errorText = ""
|
||||
TProxyConfigModel.setAcmeEmail(e)
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
text: qsTr("Used by Caddy to issue a Let's Encrypt certificate.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: httpsPortTextField
|
||||
enabled: fieldsEditable
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 8
|
||||
headerText: qsTr("HTTPS port")
|
||||
textField.placeholderText: TProxyConfigModel.defaultPort()
|
||||
textField.maximumLength: 5
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly
|
||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||
Component.onCompleted: {
|
||||
var savedPort = port
|
||||
textField.text = (savedPort === TProxyConfigModel.defaultPort()) ? "" : savedPort
|
||||
}
|
||||
textField.onTextChanged: {
|
||||
var cur = httpsPortTextField.textField.text
|
||||
var clean = TProxyConfigModel.sanitizePortFieldText(cur)
|
||||
if (clean !== cur) {
|
||||
textField.text = clean
|
||||
textField.cursorPosition = clean.length
|
||||
}
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
textField.text = TProxyConfigModel.sanitizePortFieldText(textField.text)
|
||||
if (textField.text !== "" && !httpsPortTextField.textField.acceptableInput) {
|
||||
httpsPortTextField.errorText = qsTr("The port must be in the range of 1 to 65535")
|
||||
} else {
|
||||
httpsPortTextField.errorText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
text: qsTr("Host ports are mapped to 80 and 443 inside the container. Include the HTTPS port in the Telegram WEB proxy link. Port 80 must stay reachable from the internet for the ACME (Let's Encrypt) certificate.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: qsTr("Carrier mode")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: carrierDropDown
|
||||
enabled: fieldsEditable
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
drawerParent: root
|
||||
drawerHeight: 0.45
|
||||
headerText: qsTr("Carrier mode")
|
||||
text: TProxyConfigModel.carrierModeLabel(carrierMode)
|
||||
listView: Component {
|
||||
ListViewType {
|
||||
model: ["https", "https-lanes", "websocket", "websocket-lanes"]
|
||||
delegate: LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: TProxyConfigModel.carrierModeLabel(modelData)
|
||||
rightImageSource: modelData === carrierMode ? "qrc:/images/controls/check.svg" : ""
|
||||
rightImageColor: AmneziaStyle.color.goldenApricot
|
||||
clickedFunction: function () {
|
||||
TProxyConfigModel.setCarrierMode(modelData)
|
||||
carrierDropDown.closeTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: workersTextField
|
||||
enabled: fieldsEditable
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
headerText: qsTr("MTProxy workers")
|
||||
textField.placeholderText: TProxyConfigModel.defaultWorkers()
|
||||
textField.text: workers
|
||||
textField.maximumLength: 2
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly
|
||||
textField.validator: IntValidator {
|
||||
bottom: 1
|
||||
top: TProxyConfigModel.maxWorkers()
|
||||
}
|
||||
textField.onTextChanged: {
|
||||
var cur = workersTextField.textField.text
|
||||
if (cur === "") {
|
||||
return
|
||||
}
|
||||
var n = parseInt(cur, 10)
|
||||
var maxW = TProxyConfigModel.maxWorkers()
|
||||
if (isNaN(n) || n < 1) {
|
||||
n = 1
|
||||
}
|
||||
if (n > maxW) {
|
||||
n = maxW
|
||||
}
|
||||
var clamped = String(n)
|
||||
if (clamped !== cur) {
|
||||
textField.text = clamped
|
||||
textField.cursorPosition = clamped.length
|
||||
}
|
||||
}
|
||||
textField.onEditingFinished: {
|
||||
var w = TProxyConfigModel.sanitizeWorkersFieldText(textField.text)
|
||||
textField.text = w
|
||||
TProxyConfigModel.setWorkers(w)
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
text: qsTr("Save")
|
||||
enabled: fieldsEditable && !root.tproxyNetworkBlocked
|
||||
clickedFunc: function () {
|
||||
hostnameTextField.errorText = ""
|
||||
emailTextField.errorText = ""
|
||||
httpsPortTextField.errorText = ""
|
||||
|
||||
var h = TProxyConfigModel.sanitizeHostnameFieldText(hostnameTextField.textField.text)
|
||||
var e = TProxyConfigModel.sanitizeAcmeEmailFieldText(emailTextField.textField.text)
|
||||
hostnameTextField.textField.text = h
|
||||
emailTextField.textField.text = e
|
||||
httpsPortTextField.textField.text =
|
||||
TProxyConfigModel.sanitizePortFieldText(httpsPortTextField.textField.text)
|
||||
|
||||
var hasError = false
|
||||
var portErr = qsTr("The port must be in the range of 1 to 65535")
|
||||
|
||||
var httpsPortValue = httpsPortTextField.textField.text === ""
|
||||
? TProxyConfigModel.defaultPort()
|
||||
: httpsPortTextField.textField.text
|
||||
// HTTP port (ACME) is fixed to the default (80): Let's Encrypt HTTP-01
|
||||
// always hits :80, so it is not user-editable here.
|
||||
var httpPortValue = TProxyConfigModel.defaultHttpPort()
|
||||
|
||||
if (!TProxyConfigModel.isValidHostname(h)) {
|
||||
hostnameTextField.errorText =
|
||||
qsTr("Enter a lowercase DNS hostname (A record to this server)")
|
||||
hasError = true
|
||||
}
|
||||
if (!TProxyConfigModel.isValidAcmeEmail(e)) {
|
||||
emailTextField.errorText = qsTr("Enter a valid email for the TLS certificate")
|
||||
hasError = true
|
||||
}
|
||||
if (httpsPortTextField.textField.text !== ""
|
||||
&& !httpsPortTextField.textField.acceptableInput) {
|
||||
httpsPortTextField.errorText = portErr
|
||||
hasError = true
|
||||
}
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
|
||||
previousHostname = TProxyConfigModel.getHostname()
|
||||
previousEmail = TProxyConfigModel.getAcmeEmail()
|
||||
previousSecret = secret
|
||||
TProxyConfigModel.setHostname(h)
|
||||
TProxyConfigModel.setAcmeEmail(e)
|
||||
TProxyConfigModel.setPort(httpsPortValue)
|
||||
TProxyConfigModel.setHttpPort(httpPortValue)
|
||||
TProxyConfigModel.setWorkers(TProxyConfigModel.sanitizeWorkersFieldText(workersTextField.textField.text))
|
||||
isUpdating = true
|
||||
root.scheduleUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,80 @@ PageType {
|
||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: tproxyHostname
|
||||
|
||||
visible: isTProxy
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Hostname")
|
||||
textField.placeholderText: qsTr("proxy.example.com")
|
||||
textField.onTextChanged: {
|
||||
var cur = tproxyHostname.textField.text
|
||||
var clean = TProxyConfigModel.sanitizeHostnameFieldText(cur)
|
||||
if (clean !== cur) {
|
||||
textField.text = clean
|
||||
textField.cursorPosition = clean.length
|
||||
return
|
||||
}
|
||||
if (TProxyConfigModel.isHostnameTypingIncomplete(cur)) {
|
||||
tproxyHostname.errorText = ""
|
||||
return
|
||||
}
|
||||
if (!TProxyConfigModel.isValidHostname(clean)) {
|
||||
tproxyHostname.errorText = qsTr("Use lowercase letters, digits, dots and hyphens")
|
||||
return
|
||||
}
|
||||
tproxyHostname.errorText = ""
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: tproxyEmail
|
||||
|
||||
visible: isTProxy
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("ACME email")
|
||||
textField.placeholderText: qsTr("you@example.com")
|
||||
textField.onTextChanged: {
|
||||
var cur = tproxyEmail.textField.text
|
||||
var clean = TProxyConfigModel.sanitizeAcmeEmailFieldText(cur)
|
||||
if (clean !== cur) {
|
||||
textField.text = clean
|
||||
textField.cursorPosition = clean.length
|
||||
return
|
||||
}
|
||||
if (TProxyConfigModel.isAcmeEmailTypingIncomplete(cur)) {
|
||||
tproxyEmail.errorText = ""
|
||||
return
|
||||
}
|
||||
if (!TProxyConfigModel.isValidAcmeEmail(clean)) {
|
||||
tproxyEmail.errorText = qsTr("Enter a valid email for the TLS certificate")
|
||||
return
|
||||
}
|
||||
tproxyEmail.errorText = ""
|
||||
}
|
||||
}
|
||||
|
||||
CaptionTextType {
|
||||
visible: isTProxy
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
text: qsTr("Needs a DNS A record and free host ports for HTTP (ACME) and HTTPS. Classic Telegram MTProxy links will not work.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: 16
|
||||
@@ -235,7 +309,47 @@ PageType {
|
||||
text: qsTr("Install")
|
||||
|
||||
clickedFunc: function() {
|
||||
if (!port.textField.acceptableInput &&
|
||||
if (isTProxy) {
|
||||
tproxyHostname.errorText = ""
|
||||
tproxyEmail.errorText = ""
|
||||
port.errorText = ""
|
||||
|
||||
var host = TProxyConfigModel.sanitizeHostnameFieldText(tproxyHostname.textField.text)
|
||||
var email = TProxyConfigModel.sanitizeAcmeEmailFieldText(tproxyEmail.textField.text)
|
||||
tproxyHostname.textField.text = host
|
||||
tproxyEmail.textField.text = email
|
||||
port.textField.text = TProxyConfigModel.sanitizePortFieldText(port.textField.text)
|
||||
|
||||
var hasError = false
|
||||
var portErr = qsTr("The port must be in the range of 1 to 65535")
|
||||
|
||||
// Empty port means "use the default" (443), consistent with the settings page.
|
||||
var portValue = port.textField.text === ""
|
||||
? TProxyConfigModel.defaultPort()
|
||||
: port.textField.text
|
||||
|
||||
if (!TProxyConfigModel.isValidHostname(host)) {
|
||||
tproxyHostname.errorText = qsTr("Enter a lowercase DNS hostname")
|
||||
hasError = true
|
||||
}
|
||||
if (!TProxyConfigModel.isValidAcmeEmail(email)) {
|
||||
tproxyEmail.errorText = qsTr("Enter a valid email for the TLS certificate")
|
||||
hasError = true
|
||||
}
|
||||
if (port.textField.text !== "" && !port.textField.acceptableInput) {
|
||||
port.errorText = portErr
|
||||
hasError = true
|
||||
}
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
|
||||
port.textField.text = portValue
|
||||
TProxyConfigModel.setHostname(host)
|
||||
TProxyConfigModel.setAcmeEmail(email)
|
||||
TProxyConfigModel.setPort(portValue)
|
||||
TProxyConfigModel.setHttpPort(TProxyConfigModel.defaultHttpPort())
|
||||
} else if (!port.textField.acceptableInput &&
|
||||
ContainerProps.containerTypeToString(dockerContainer) !== "torwebsite" &&
|
||||
ContainerProps.containerTypeToString(dockerContainer) !== "ikev2") {
|
||||
port.errorText = qsTr("The port must be in the range of 1 to 65535")
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
<file>Pages2/PageServiceDnsSettings.qml</file>
|
||||
<file>Pages2/PageServiceMtProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceTelemtSettings.qml</file>
|
||||
<file>Pages2/PageServiceTProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceSftpSettings.qml</file>
|
||||
<file>Pages2/PageServiceSocksProxySettings.qml</file>
|
||||
<file>Pages2/PageServiceTorWebsiteSettings.qml</file>
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace PageLoader
|
||||
PageServiceSocksProxySettings,
|
||||
PageServiceMtProxySettings,
|
||||
PageServiceTelemtSettings,
|
||||
PageServiceTProxySettings,
|
||||
|
||||
PageSetupWizardStart,
|
||||
PageSetupWizardCredentials,
|
||||
|
||||
Reference in New Issue
Block a user