ZaStoGram_desktop/Telegram/SourceFiles/mtproto/proxy/capabilities.cpp
loop-uh d30a63783b fix
2026-07-08 16:28:12 +03:00

457 lines
13 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "mtproto/proxy/capabilities.h"
#include "base/bytes.h"
#include "base/qt/qt_string_view.h"
#include <QtCore/QCryptographicHash>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QSaveFile>
#include <algorithm>
namespace MTP {
namespace {
constexpr auto kMaxStoredRoutes = 16;
constexpr auto kMtproxyRelayProofTtl = crl::time(24 * 60 * 60 * 1000);
[[nodiscard]] QByteArray BytesToQByteArray(bytes::const_span data) {
auto result = QByteArray();
result.reserve(int(data.size()));
for (const auto byte : data) {
result.append(char(gsl::to_integer<unsigned char>(byte)));
}
return result;
}
[[nodiscard]] QString HashBytes(bytes::const_span data) {
const auto hash = QCryptographicHash::hash(
BytesToQByteArray(data),
QCryptographicHash::Sha256);
return QString::fromLatin1(hash.toHex());
}
[[nodiscard]] QString HashText(const QString &text) {
const auto hash = QCryptographicHash::hash(
text.toUtf8(),
QCryptographicHash::Sha256);
return QString::fromLatin1(hash.toHex());
}
[[nodiscard]] QString DomainFromSecret(bytes::const_span secret) {
if (secret.size() <= 17) {
return QString();
}
return QString::fromUtf8(BytesToQByteArray(secret.subspan(17)));
}
[[nodiscard]] QString ProxyCapabilityHost(const ProxyData &proxy) {
return proxy.originalHost.isEmpty()
? proxy.host
: proxy.originalHost;
}
[[nodiscard]] QString ProxyCapabilitySecretHash(const ProxyData &proxy) {
if (proxy.type == ProxyData::Type::Mtproto) {
const auto secret = proxy.secretFromMtprotoPassword();
if (!secret.empty()) {
return HashBytes(bytes::make_span(secret));
}
}
return (proxy.type == ProxyData::Type::Mtproto)
? HashText(proxy.password)
: HashText(proxy.user + ':' + proxy.password);
}
[[nodiscard]] QString ProxyCapabilityDomain(const ProxyData &proxy) {
if (proxy.type != ProxyData::Type::Mtproto) {
return QString();
}
const auto secret = proxy.secretFromMtprotoPassword();
return secret.empty()
? QString()
: DomainFromSecret(bytes::make_span(secret));
}
[[nodiscard]] QString TransportName(ProxyCapabilityTransport value) {
switch (value) {
case ProxyCapabilityTransport::Tcp:
return u"Tcp"_q;
case ProxyCapabilityTransport::MtproxyFakeTlsTcp:
return u"MtproxyFakeTlsTcp"_q;
case ProxyCapabilityTransport::Wss:
return u"Wss"_q;
case ProxyCapabilityTransport::Unknown:
return QString();
}
return QString();
}
[[nodiscard]] ProxyCapabilityTransport TransportFromName(
const QString &value) {
if (value == u"Tcp"_q) {
return ProxyCapabilityTransport::Tcp;
} else if (value == u"MtproxyFakeTlsTcp"_q) {
return ProxyCapabilityTransport::MtproxyFakeTlsTcp;
} else if (value == u"Wss"_q) {
return ProxyCapabilityTransport::Wss;
}
return ProxyCapabilityTransport::Unknown;
}
[[nodiscard]] QString ProfileName(ProxyTlsProfile value) {
switch (value) {
case ProxyTlsProfile::Auto:
return u"Auto"_q;
case ProxyTlsProfile::Firefox:
return u"Firefox"_q;
case ProxyTlsProfile::AndroidChrome:
return u"AndroidChrome"_q;
case ProxyTlsProfile::Yandex:
return u"Yandex"_q;
case ProxyTlsProfile::FirefoxAndroid:
return u"FirefoxAndroid"_q;
case ProxyTlsProfile::AndroidOkHttp:
return u"AndroidOkHttp"_q;
case ProxyTlsProfile::AutoRotate:
return u"AutoRotate"_q;
case ProxyTlsProfile::ChromeModern:
return u"ChromeModern"_q;
}
return u"Auto"_q;
}
[[nodiscard]] ProxyTlsProfile ProfileFromName(const QString &value) {
if (value == u"Firefox"_q) {
return ProxyTlsProfile::Firefox;
} else if (value == u"AndroidChrome"_q) {
return ProxyTlsProfile::AndroidChrome;
} else if (value == u"Yandex"_q) {
return ProxyTlsProfile::Yandex;
} else if (value == u"FirefoxAndroid"_q) {
return ProxyTlsProfile::FirefoxAndroid;
} else if (value == u"AndroidOkHttp"_q) {
return ProxyTlsProfile::AndroidOkHttp;
} else if (value == u"AutoRotate"_q) {
return ProxyTlsProfile::AutoRotate;
} else if (value == u"ChromeModern"_q) {
return ProxyTlsProfile::ChromeModern;
}
return ProxyTlsProfile::Auto;
}
[[nodiscard]] std::vector<QString> ReadStringVector(
const QJsonObject &object,
const QString &key) {
auto result = std::vector<QString>();
const auto array = object.value(key).toArray();
result.reserve(array.size());
for (const auto &value : array) {
const auto route = value.toString();
if (!route.isEmpty()) {
result.push_back(route);
}
}
return result;
}
[[nodiscard]] QJsonArray WriteStringVector(
const std::vector<QString> &values) {
auto result = QJsonArray();
for (const auto &value : values) {
result.append(value);
}
return result;
}
[[nodiscard]] ProxyCapabilityCard ReadCard(const QJsonObject &object) {
auto result = ProxyCapabilityCard();
result.proxyKey = object.value("proxyKey").toString();
result.lastGoodTransport = TransportFromName(
object.value("lastGoodTransport").toString());
result.lastGoodRoute = object.value("lastGoodRoute").toString();
result.lastGoodProfile = ProfileFromName(
object.value("lastGoodProfile").toString());
result.lastGoodRecipeLevel = object.value(
"lastGoodRecipeLevel").toInt();
result.relayProven = object.value("relayProven").toBool(false);
result.autoRotateAllowed = object.value(
"autoRotateAllowed").toBool(true);
result.wssAllowed = object.value("wssAllowed").toBool(true);
result.syntheticPskAllowed = object.value(
"syntheticPskAllowed").toBool(false);
result.fragmentationAllowed = object.value(
"fragmentationAllowed").toBool(false);
result.lastSuccessAt = crl::time(
object.value("lastSuccessAt").toDouble());
const auto relayProvenAt = crl::time(
object.value("relayProvenAt").toDouble());
result.relayProvenAt = relayProvenAt
? relayProvenAt
: result.relayProven
? result.lastSuccessAt
: 0;
result.lastFailureClass = object.value(
"lastFailureClass").toString();
result.badRoutes = ReadStringVector(object, "badRoutes");
result.goodRoutes = ReadStringVector(object, "goodRoutes");
result.wssBlockedUntil = crl::time(
object.value("wssBlockedUntil").toDouble());
return result;
}
[[nodiscard]] QJsonObject WriteCard(const ProxyCapabilityCard &card) {
auto result = QJsonObject();
result.insert("proxyKey", card.proxyKey);
result.insert(
"lastGoodTransport",
TransportName(card.lastGoodTransport));
result.insert("lastGoodRoute", card.lastGoodRoute);
result.insert("lastGoodProfile", ProfileName(card.lastGoodProfile));
result.insert("lastGoodRecipeLevel", card.lastGoodRecipeLevel);
result.insert("relayProven", card.relayProven);
result.insert("autoRotateAllowed", card.autoRotateAllowed);
result.insert("wssAllowed", card.wssAllowed);
result.insert("syntheticPskAllowed", card.syntheticPskAllowed);
result.insert("fragmentationAllowed", card.fragmentationAllowed);
result.insert("lastSuccessAt", double(card.lastSuccessAt));
result.insert("relayProvenAt", double(card.relayProvenAt));
result.insert("lastFailureClass", card.lastFailureClass);
result.insert("badRoutes", WriteStringVector(card.badRoutes));
result.insert("goodRoutes", WriteStringVector(card.goodRoutes));
result.insert("wssBlockedUntil", double(card.wssBlockedUntil));
return result;
}
void AddRoute(std::vector<QString> &routes, const QString &routeKey) {
if (routeKey.isEmpty()) {
return;
}
const auto i = std::find(begin(routes), end(routes), routeKey);
if (i != end(routes)) {
routes.erase(i);
}
routes.insert(routes.begin(), routeKey);
if (routes.size() > kMaxStoredRoutes) {
routes.resize(kMaxStoredRoutes);
}
}
void RemoveRoute(std::vector<QString> &routes, const QString &routeKey) {
const auto i = std::find(begin(routes), end(routes), routeKey);
if (i != end(routes)) {
routes.erase(i);
}
}
[[nodiscard]] bool FreshMtproxyRelayProof(
const ProxyCapabilityCard &card) {
return card.relayProven
&& card.relayProvenAt
&& (crl::now() - card.relayProvenAt <= kMtproxyRelayProofTtl);
}
[[nodiscard]] ProxyCapabilityCard CardWithAgedRelayProof(
ProxyCapabilityCard card) {
if (!FreshMtproxyRelayProof(card)) {
card.relayProven = false;
card.relayProvenAt = 0;
}
return card;
}
[[nodiscard]] bool HardMtproxyFailureInvalidatesRelayProof(
const QString &failureClass) {
return (failureClass == u"client_hello_sent_no_server_hello"_q)
|| (failureClass == u"tls_alert_after_client_hello"_q)
|| (failureClass == u"server_hello_hmac_mismatch"_q);
}
} // namespace
ProxyCapabilityCache::ProxyCapabilityCache(Fn<QString()> path)
: _path(std::move(path)) {
}
ProxyCapabilityCard ProxyCapabilityCache::lookup(const ProxyData &proxy) {
return lookup(ProxyCapabilityKey(proxy));
}
ProxyCapabilityCard ProxyCapabilityCache::lookup(const QString &proxyKey) {
QMutexLocker lock(&_mutex);
load();
const auto i = _cards.find(proxyKey);
if (i != end(_cards)) {
return CardWithAgedRelayProof(i->second);
}
auto result = ProxyCapabilityCard();
result.proxyKey = proxyKey;
return result;
}
bool ProxyCapabilityCache::wssAllowed(const ProxyData &proxy) {
const auto card = lookup(proxy);
return card.wssAllowed || card.wssBlockedUntil <= crl::now();
}
void ProxyCapabilityCache::noteWssRemoteClosed(
const ProxyData &proxy,
crl::time ttl) {
const auto key = ProxyCapabilityKey(proxy);
if (key.isEmpty()) {
return;
}
QMutexLocker lock(&_mutex);
load();
auto &card = _cards[key];
card.proxyKey = key;
card.wssAllowed = false;
card.wssBlockedUntil = crl::now() + ttl;
card.lastFailureClass = u"remote_closed"_q;
save();
}
void ProxyCapabilityCache::noteMtproxySuccess(
const QString &proxyKey,
const QString &routeKey,
const QString &lastGoodRoute,
ProxyTlsProfile sentProfile,
const ProxyStealthOptions &stealth,
int recipeLevel,
bool relayProven) {
if (proxyKey.isEmpty()) {
return;
}
QMutexLocker lock(&_mutex);
load();
const auto now = crl::now();
auto &card = _cards[proxyKey];
card.proxyKey = proxyKey;
card.lastGoodTransport = ProxyCapabilityTransport::MtproxyFakeTlsTcp;
card.lastGoodRoute = lastGoodRoute;
card.lastGoodProfile = sentProfile;
card.lastGoodRecipeLevel = recipeLevel;
card.relayProven = relayProven;
card.relayProvenAt = relayProven ? now : 0;
card.autoRotateAllowed = false;
card.lastSuccessAt = now;
card.lastFailureClass.clear();
card.syntheticPskAllowed = stealth.syntheticPsk;
card.fragmentationAllowed = (stealth.clientHelloFragmentation
!= ProxyClientHelloFragmentation::Off);
AddRoute(card.goodRoutes, routeKey);
RemoveRoute(card.badRoutes, routeKey);
save();
}
void ProxyCapabilityCache::noteMtproxyFailure(
const QString &proxyKey,
const QString &routeKey,
const QString &failureClass) {
if (proxyKey.isEmpty()) {
return;
}
QMutexLocker lock(&_mutex);
load();
auto &card = _cards[proxyKey];
card.proxyKey = proxyKey;
card.lastFailureClass = failureClass;
if (HardMtproxyFailureInvalidatesRelayProof(failureClass)) {
card.relayProven = false;
card.relayProvenAt = 0;
}
AddRoute(card.badRoutes, routeKey);
save();
}
void ProxyCapabilityCache::noteMtproxyRelayFailure(
const QString &proxyKey,
const QString &routeKey,
const QString &failureClass) {
if (proxyKey.isEmpty()) {
return;
}
QMutexLocker lock(&_mutex);
load();
auto &card = _cards[proxyKey];
card.proxyKey = proxyKey;
card.relayProven = false;
card.relayProvenAt = 0;
card.lastFailureClass = failureClass;
AddRoute(card.badRoutes, routeKey);
RemoveRoute(card.goodRoutes, routeKey);
save();
}
QString ProxyCapabilityCache::path() const {
return _path ? _path() : QString();
}
void ProxyCapabilityCache::load() {
if (_loaded) {
return;
}
_loaded = true;
const auto data = [&] {
auto file = QFile(path());
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
}();
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
return;
}
const auto array = document.object().value("cards").toArray();
for (const auto &value : array) {
const auto object = value.toObject();
auto card = ReadCard(object);
if (!card.proxyKey.isEmpty()) {
_cards[card.proxyKey] = std::move(card);
}
}
}
void ProxyCapabilityCache::save() {
auto cards = QJsonArray();
for (const auto &[key, card] : _cards) {
if (!key.isEmpty()) {
cards.append(WriteCard(card));
}
}
auto root = QJsonObject();
root.insert("cards", cards);
auto file = QSaveFile(path());
if (file.open(QIODevice::WriteOnly)) {
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
file.commit();
}
}
QString ProxyCapabilityKey(const ProxyData &proxy) {
const auto host = ProxyCapabilityHost(proxy);
if (host.isEmpty() || proxy.port <= 0) {
return QString();
}
const auto port = QString::number(proxy.port);
return host
+ ':'
+ port
+ ':'
+ QString::number(int(proxy.type))
+ ':'
+ ProxyCapabilitySecretHash(proxy)
+ ':'
+ ProxyCapabilityDomain(proxy);
}
} // namespace MTP