mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
541 lines
16 KiB
C++
541 lines
16 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/mtproto_proxy_data.h"
|
|
|
|
#include "base/qthelp_url.h"
|
|
#include "base/qt/qt_string_view.h"
|
|
|
|
#include <QtCore/QCryptographicHash>
|
|
#include <QtCore/QMessageAuthenticationCode>
|
|
#include <QtCore/QUrl>
|
|
#include <QtCore/QUrlQuery>
|
|
#include <QtNetwork/QHostAddress>
|
|
|
|
namespace MTP {
|
|
namespace {
|
|
|
|
[[nodiscard]] bool IsHexMtprotoPassword(const QString &password) {
|
|
const auto size = password.size();
|
|
if (size < 32 || size % 2 == 1) {
|
|
return false;
|
|
}
|
|
const auto bad = [](QChar ch) {
|
|
const auto code = ch.unicode();
|
|
return (code < 'a' || code > 'f')
|
|
&& (code < 'A' || code > 'F')
|
|
&& (code < '0' || code > '9');
|
|
};
|
|
const auto i = std::find_if(password.begin(), password.end(), bad);
|
|
return (i == password.end());
|
|
}
|
|
|
|
[[nodiscard]] ProxyData::Status HexMtprotoPasswordStatus(
|
|
const QString &password) {
|
|
const auto size = password.size() / 2;
|
|
const auto type1 = password[0].toLower();
|
|
const auto type2 = password[1].toLower();
|
|
const auto valid = (size == 16)
|
|
|| (size == 17 && (type1 == 'd') && (type2 == 'd'))
|
|
|| (size >= 21 && (type1 == 'e') && (type2 == 'e'));
|
|
if (valid) {
|
|
return ProxyData::Status::Valid;
|
|
} else if (size < 16) {
|
|
return ProxyData::Status::Invalid;
|
|
}
|
|
return ProxyData::Status::Unsupported;
|
|
}
|
|
|
|
[[nodiscard]] bytes::vector SecretFromHexMtprotoPassword(
|
|
const QString &password) {
|
|
Expects(password.size() % 2 == 0);
|
|
|
|
const auto size = password.size() / 2;
|
|
const auto fromHex = [](QChar ch) -> int {
|
|
const auto code = int(ch.unicode());
|
|
if (code >= '0' && code <= '9') {
|
|
return (code - '0');
|
|
} else if (code >= 'A' && code <= 'F') {
|
|
return 10 + (code - 'A');
|
|
} else if (ch >= 'a' && ch <= 'f') {
|
|
return 10 + (code - 'a');
|
|
}
|
|
Unexpected("Code in ProxyData fromHex.");
|
|
};
|
|
auto result = bytes::vector(size);
|
|
for (auto i = 0; i != size; ++i) {
|
|
const auto high = fromHex(password[2 * i]);
|
|
const auto low = fromHex(password[2 * i + 1]);
|
|
if (high < 0 || low < 0) {
|
|
return {};
|
|
}
|
|
result[i] = static_cast<bytes::type>(high * 16 + low);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QStringView Base64UrlInner(const QString &password) {
|
|
Expects(password.size() > 2);
|
|
|
|
// Skip one or two '=' at the end of the string.
|
|
return base::StringViewMid(password, 0, [&] {
|
|
auto result = password.size();
|
|
for (auto i = 0; i != 2; ++i) {
|
|
const auto prev = result - 1;
|
|
if (password[prev] != '=') {
|
|
break;
|
|
}
|
|
result = prev;
|
|
}
|
|
return result;
|
|
}());
|
|
}
|
|
|
|
[[nodiscard]] bool IsBase64UrlMtprotoPassword(const QString &password) {
|
|
const auto size = password.size();
|
|
if (size < 22 || size % 4 == 1) {
|
|
return false;
|
|
}
|
|
const auto bad = [](QChar ch) {
|
|
const auto code = ch.unicode();
|
|
return (code < 'a' || code > 'z')
|
|
&& (code < 'A' || code > 'Z')
|
|
&& (code < '0' || code > '9')
|
|
&& (code != '_')
|
|
&& (code != '-');
|
|
};
|
|
const auto inner = Base64UrlInner(password);
|
|
const auto begin = inner.data();
|
|
const auto end = begin + inner.size();
|
|
return (std::find_if(begin, end, bad) == end);
|
|
}
|
|
|
|
[[nodiscard]] ProxyData::Status Base64UrlMtprotoPasswordStatus(
|
|
const QString &password) {
|
|
// IncorrectSecret
|
|
const auto inner = Base64UrlInner(password);
|
|
const auto size = (inner.size() * 3) / 4;
|
|
const auto valid = (size == 16)
|
|
|| (size == 17
|
|
&& (password[0] == '3')
|
|
&& ((password[1] >= 'Q' && password[1] <= 'Z')
|
|
|| (password[1] >= 'a' && password[1] <= 'f')))
|
|
|| (size >= 21
|
|
&& (password[0] == '7')
|
|
&& (password[1] >= 'g')
|
|
&& (password[1] <= 'v'));
|
|
const auto incorrect = (size >= 21
|
|
&& password[0].toLower() == 'e'
|
|
&& password[1].toLower() == 'e');
|
|
if (size < 16) {
|
|
return ProxyData::Status::Invalid;
|
|
} else if (valid) {
|
|
return ProxyData::Status::Valid;
|
|
} else if (incorrect) {
|
|
return ProxyData::Status::IncorrectSecret;
|
|
}
|
|
return ProxyData::Status::Unsupported;
|
|
}
|
|
|
|
[[nodiscard]] bytes::vector SecretFromBase64UrlMtprotoPassword(
|
|
const QString &password) {
|
|
const auto result = QByteArray::fromBase64(
|
|
password.toLatin1(),
|
|
QByteArray::Base64UrlEncoding);
|
|
return bytes::make_vector(bytes::make_span(result));
|
|
}
|
|
|
|
// The WHATWG URL "ends in a number" rule: a host whose last label is
|
|
// all ASCII digits or 0x-prefixed hex is an IPv4 address (possibly in
|
|
// shorthand form like 127.1 or 0x7f.1), so the IP-literal policy below
|
|
// does not depend on QHostAddress recognising every shorthand.
|
|
[[nodiscard]] bool LastLabelIsNumeric(const QString &host) {
|
|
const auto label = host.mid(host.lastIndexOf('.') + 1);
|
|
if (label.isEmpty()) {
|
|
return false;
|
|
}
|
|
const auto hex = label.startsWith(u"0x"_q, Qt::CaseInsensitive);
|
|
const auto digits = hex ? label.mid(2) : label;
|
|
for (const auto ch : digits) {
|
|
const auto code = ch.unicode();
|
|
const auto decimal = (code >= '0' && code <= '9');
|
|
const auto alpha = (code >= 'a' && code <= 'f')
|
|
|| (code >= 'A' && code <= 'F');
|
|
if (!decimal && !(hex && alpha)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The root deployment keeps the frozen v1 context byte-for-byte; a base
|
|
// path binds the capability to the (host, path) pair with its own context,
|
|
// so a capability minted for one prefix is useless on another.
|
|
[[nodiscard]] QString ComputeWebProxyBridgeCapability(
|
|
const QString &host,
|
|
const QString &basePath,
|
|
const QByteArray &key) {
|
|
const auto context = basePath.isEmpty()
|
|
? (QByteArray("tdesktop-web-proxy-bridge-v1\n") + host.toLatin1())
|
|
: (QByteArray("tdesktop-web-proxy-bridge-v2\n")
|
|
+ host.toLatin1()
|
|
+ '\n'
|
|
+ basePath.toLatin1());
|
|
return QString::fromLatin1(QMessageAuthenticationCode::hash(
|
|
context,
|
|
key,
|
|
QCryptographicHash::Sha256
|
|
).toBase64(
|
|
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
|
}
|
|
|
|
// The address field accepts a pasted URL, so the scheme is removed before the
|
|
// address is parsed and only the canonical form is stored. `http://` is left in
|
|
// place, and therefore rejected: HTTPS and port 443 are fixed for a WEB proxy.
|
|
[[nodiscard]] QStringView StripWebProxyScheme(const QString &value) {
|
|
const auto scheme = u"https://"_q;
|
|
auto result = QStringView(value).trimmed();
|
|
if (result.startsWith(scheme, Qt::CaseInsensitive)) {
|
|
result = result.mid(scheme.size());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QStringView TrimWebProxyBasePath(const QString &value) {
|
|
auto result = QStringView(value).trimmed();
|
|
if (result.startsWith('/')) {
|
|
result = result.mid(1);
|
|
}
|
|
if (result.endsWith('/')) {
|
|
result = result.chopped(1);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// One or more `/`-separated segments, each starting with an ASCII letter or
|
|
// digit and continuing with those, `-` or `_`. `.` is not in the alphabet at
|
|
// all, so `.` and `..` segments cannot appear and the value never needs dot
|
|
// segment resolution. Empty segments are rejected, so the canonical value and
|
|
// the wire path stay the same string.
|
|
[[nodiscard]] QString NormalizeWebProxyBasePath(const QString &value) {
|
|
const auto result = TrimWebProxyBasePath(value);
|
|
if (result.isEmpty() || result.size() > 128) {
|
|
return QString();
|
|
}
|
|
auto segmentStart = true;
|
|
for (const auto character : result) {
|
|
const auto code = character.unicode();
|
|
if (code == '/') {
|
|
if (segmentStart) {
|
|
return QString();
|
|
}
|
|
segmentStart = true;
|
|
continue;
|
|
}
|
|
const auto letter = (code >= 'a' && code <= 'z')
|
|
|| (code >= 'A' && code <= 'Z');
|
|
const auto digit = (code >= '0' && code <= '9');
|
|
const auto extra = (code == '-' || code == '_');
|
|
if (!letter && !digit && !(extra && !segmentStart)) {
|
|
return QString();
|
|
}
|
|
segmentStart = false;
|
|
}
|
|
return segmentStart ? QString() : result.toString();
|
|
}
|
|
|
|
[[nodiscard]] bool ValidWebProxyBasePath(const QString &value) {
|
|
return TrimWebProxyBasePath(value).isEmpty()
|
|
|| !NormalizeWebProxyBasePath(value).isEmpty();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
QString NormalizeWebProxyHost(const QString &value) {
|
|
const auto input = value.trimmed();
|
|
if (input.isEmpty()
|
|
|| input.contains(':')
|
|
|| input.contains('/')
|
|
|| input.contains('?')
|
|
|| input.contains('#')
|
|
|| input.contains('@')
|
|
|| input.endsWith('.')) {
|
|
return QString();
|
|
}
|
|
const auto result = QString::fromLatin1(QUrl::toAce(input)).toLower();
|
|
if (result.isEmpty()
|
|
|| result.size() > 253
|
|
|| !result.contains('.')) {
|
|
return QString();
|
|
}
|
|
for (const auto &label : result.split('.')) {
|
|
if (label.isEmpty()
|
|
|| label.size() > 63
|
|
|| label.front() == '-'
|
|
|| label.back() == '-') {
|
|
return QString();
|
|
}
|
|
for (const auto ch : label) {
|
|
if (!ch.isLetterOrNumber() && ch != '-') {
|
|
return QString();
|
|
}
|
|
}
|
|
}
|
|
if (LastLabelIsNumeric(result)) {
|
|
return QString();
|
|
}
|
|
auto address = QHostAddress();
|
|
return address.setAddress(result) ? QString() : result;
|
|
}
|
|
|
|
QString WebProxyBridgeCapability(const ProxyData &proxy) {
|
|
Expects(proxy.type == ProxyData::Type::Web);
|
|
Expects(proxy.host == NormalizeWebProxyHost(proxy.host));
|
|
Expects(proxy.webBasePath()
|
|
== NormalizeWebProxyBasePath(proxy.webBasePath()));
|
|
|
|
#ifndef NDEBUG
|
|
[[maybe_unused]] static const auto checked = [] {
|
|
Assert(NormalizeWebProxyHost(u" Proxy.Example.COM "_q)
|
|
== u"proxy.example.com"_q);
|
|
Assert(NormalizeWebProxyHost(u"bücher.example"_q)
|
|
== u"xn--bcher-kva.example"_q);
|
|
Assert(NormalizeWebProxyHost(u"bücher.de"_q)
|
|
== u"xn--bcher-kva.de"_q);
|
|
Assert(NormalizeWebProxyHost(u"xn--strae-oqa.example"_q)
|
|
== u"xn--strae-oqa.example"_q);
|
|
Assert(NormalizeWebProxyHost(u"localhost"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"127.0.0.1"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"127.1"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"0x7f.1"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"0177.0.0.1"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"1.2.3"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"site.example:443"_q).isEmpty());
|
|
Assert(NormalizeWebProxyHost(u"site..example"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u" /dobry-cola-super-app/ "_q)
|
|
== u"dobry-cola-super-app"_q);
|
|
Assert(NormalizeWebProxyBasePath(u"a"_q) == u"a"_q);
|
|
Assert(NormalizeWebProxyBasePath(QString()).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"-lead"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"MixedCase"_q)
|
|
== u"MixedCase"_q);
|
|
Assert(NormalizeWebProxyBasePath(u" /two/segments/ "_q)
|
|
== u"two/segments"_q);
|
|
Assert(NormalizeWebProxyBasePath(u"with space"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"empty//segment"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"trailing//"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"a/-lead"_q).isEmpty());
|
|
Assert(StripWebProxyScheme(u" HTTPS://Proxy.Example.COM/App/ "_q)
|
|
== u"Proxy.Example.COM/App/"_q);
|
|
Assert(StripWebProxyScheme(u"http://proxy.example.com"_q)
|
|
== u"http://proxy.example.com"_q);
|
|
Assert(NormalizeWebProxyBasePath(u"per%20cent"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u"dot.ted"_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(u".."_q).isEmpty());
|
|
Assert(NormalizeWebProxyBasePath(QString(129, QChar('a'))).isEmpty());
|
|
Assert(!NormalizeWebProxyBasePath(QString(128, QChar('a'))).isEmpty());
|
|
Assert(ValidWebProxyBasePath(QString()));
|
|
Assert(ValidWebProxyBasePath(u"/"_q));
|
|
Assert(!ValidWebProxyBasePath(u"empty//segment"_q));
|
|
const auto plain = QByteArray::fromHex(
|
|
"000102030405060708090a0b0c0d0e0f");
|
|
const auto padded = QByteArray::fromHex(
|
|
"dd000102030405060708090a0b0c0d0e0f");
|
|
const auto path = u"dobry-cola-super-app"_q;
|
|
Assert(ComputeWebProxyBridgeCapability(
|
|
u"proxy.example.com"_q,
|
|
QString(),
|
|
plain) == u"MHLEY5PmW1GWqJkSrlmJpvJUiLhBH_QKy6yKg8a0JPk"_q);
|
|
Assert(ComputeWebProxyBridgeCapability(
|
|
u"proxy.example.com"_q,
|
|
QString(),
|
|
padded) == u"IpJrt3e7sKtzPyoXy6w-Zj6GGEvsvclN66JzQEfPYLA"_q);
|
|
Assert(ComputeWebProxyBridgeCapability(
|
|
u"proxy.example.com"_q,
|
|
path,
|
|
plain) == u"hHz99Xs93EN1j91G9gpNepXwGNNt5YdAFkEVk_LlqdQ"_q);
|
|
Assert(ComputeWebProxyBridgeCapability(
|
|
u"proxy.example.com"_q,
|
|
path,
|
|
padded) == u"TGUkZaevsavLbHvlNWipnRoYxgzZ51ioWvbxgGT3wHo"_q);
|
|
return true;
|
|
}();
|
|
#endif // !NDEBUG
|
|
|
|
const auto secret = proxy.secretFromMtprotoPassword();
|
|
Expects(!secret.empty());
|
|
const auto key = QByteArray(
|
|
reinterpret_cast<const char*>(secret.data()),
|
|
int(secret.size()));
|
|
return ComputeWebProxyBridgeCapability(
|
|
proxy.host,
|
|
proxy.webBasePath(),
|
|
key);
|
|
}
|
|
|
|
QString WebProxyBridgePath(const ProxyData &proxy) {
|
|
Expects(proxy.type == ProxyData::Type::Web);
|
|
|
|
const auto path = proxy.webBasePath();
|
|
return path.isEmpty() ? u"/"_q : ('/' + path + '/');
|
|
}
|
|
|
|
QString WebProxyBridgeUrl(const ProxyData &proxy) {
|
|
auto result = QUrl(u"https://"_q + proxy.host);
|
|
result.setPath(WebProxyBridgePath(proxy));
|
|
auto query = QUrlQuery();
|
|
query.addQueryItem(u"bridge"_q, WebProxyBridgeCapability(proxy));
|
|
result.setQuery(query);
|
|
return result.toString(QUrl::FullyEncoded);
|
|
}
|
|
|
|
QString ProxyData::webAddress() const {
|
|
const auto path = webBasePath();
|
|
return path.isEmpty() ? host : (host + '/' + path);
|
|
}
|
|
|
|
void ProxyData::setWebAddress(const QString &value) {
|
|
Expects(type == Type::Web);
|
|
|
|
const auto trimmed = StripWebProxyScheme(value).toString();
|
|
const auto slash = trimmed.indexOf('/');
|
|
const auto path = (slash < 0)
|
|
? QString()
|
|
: trimmed.mid(slash + 1);
|
|
host = NormalizeWebProxyHost((slash < 0)
|
|
? trimmed
|
|
: trimmed.mid(0, slash));
|
|
if (host.isEmpty() || !ValidWebProxyBasePath(path)) {
|
|
host = QString();
|
|
user = QString();
|
|
} else {
|
|
user = NormalizeWebProxyBasePath(path);
|
|
}
|
|
}
|
|
|
|
QString ProxyData::webBasePath() const {
|
|
return (type == Type::Web) ? user : QString();
|
|
}
|
|
|
|
bool ProxyData::valid() const {
|
|
return status() == Status::Valid;
|
|
}
|
|
|
|
ProxyData::Status ProxyData::status() const {
|
|
if (type == Type::Web) {
|
|
if (host.isEmpty()
|
|
|| host != NormalizeWebProxyHost(host)
|
|
|| port != 443
|
|
|| user != NormalizeWebProxyBasePath(user)) {
|
|
return Status::Invalid;
|
|
}
|
|
const auto result = MtprotoPasswordStatus(password);
|
|
if (result != Status::Valid) {
|
|
return result;
|
|
}
|
|
const auto secret = secretFromMtprotoPassword();
|
|
return (secret.size() >= 21 && secret[0] == bytes::type(0xEE))
|
|
? Status::Unsupported
|
|
: Status::Valid;
|
|
} else if (type == Type::None || host.isEmpty() || !port) {
|
|
return Status::Invalid;
|
|
} else if (type == Type::Mtproto) {
|
|
return MtprotoPasswordStatus(password);
|
|
}
|
|
return Status::Valid;
|
|
}
|
|
|
|
bool ProxyData::supportsCalls() const {
|
|
return false;// (type == Type::Socks5);
|
|
}
|
|
|
|
bool ProxyData::tryCustomResolve() const {
|
|
static const auto RegExp = QRegularExpression(
|
|
QStringLiteral("^\\d+\\.\\d+\\.\\d+\\.\\d+$")
|
|
);
|
|
return (type == Type::Socks5 || type == Type::Mtproto)
|
|
&& !qthelp::is_ipv6(host)
|
|
&& !RegExp.match(host).hasMatch();
|
|
}
|
|
|
|
bytes::vector ProxyData::secretFromMtprotoPassword() const {
|
|
Expects(type == Type::Mtproto || type == Type::Web);
|
|
|
|
if (IsHexMtprotoPassword(password)) {
|
|
return SecretFromHexMtprotoPassword(password);
|
|
} else if (IsBase64UrlMtprotoPassword(password)) {
|
|
return SecretFromBase64UrlMtprotoPassword(password);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
ProxyData::operator bool() const {
|
|
return valid();
|
|
}
|
|
|
|
bool ProxyData::operator==(const ProxyData &other) const {
|
|
if (!valid()) {
|
|
return !other.valid();
|
|
}
|
|
return (type == other.type)
|
|
&& (host == other.host)
|
|
&& (port == other.port)
|
|
&& (user == other.user)
|
|
&& (password == other.password);
|
|
}
|
|
|
|
bool ProxyData::operator!=(const ProxyData &other) const {
|
|
return !(*this == other);
|
|
}
|
|
|
|
bool ProxyData::ValidMtprotoPassword(const QString &password) {
|
|
return MtprotoPasswordStatus(password) == Status::Valid;
|
|
}
|
|
|
|
ProxyData::Status ProxyData::MtprotoPasswordStatus(const QString &password) {
|
|
if (IsHexMtprotoPassword(password)) {
|
|
return HexMtprotoPasswordStatus(password);
|
|
} else if (IsBase64UrlMtprotoPassword(password)) {
|
|
return Base64UrlMtprotoPasswordStatus(password);
|
|
}
|
|
return Status::Invalid;
|
|
}
|
|
|
|
ProxyData ToDirectIpProxy(const ProxyData &proxy, int ipIndex) {
|
|
if (!proxy.tryCustomResolve()
|
|
|| ipIndex < 0
|
|
|| ipIndex >= proxy.resolvedIPs.size()) {
|
|
return proxy;
|
|
}
|
|
return {
|
|
proxy.type,
|
|
proxy.resolvedIPs[ipIndex],
|
|
proxy.port,
|
|
proxy.user,
|
|
proxy.password
|
|
};
|
|
}
|
|
|
|
QNetworkProxy ToNetworkProxy(const ProxyData &proxy) {
|
|
if (proxy.type == ProxyData::Type::None) {
|
|
return QNetworkProxy::DefaultProxy;
|
|
} else if (proxy.type == ProxyData::Type::Mtproto
|
|
|| proxy.type == ProxyData::Type::Web) {
|
|
return QNetworkProxy::NoProxy;
|
|
}
|
|
return QNetworkProxy(
|
|
(proxy.type == ProxyData::Type::Socks5
|
|
? QNetworkProxy::Socks5Proxy
|
|
: QNetworkProxy::HttpProxy),
|
|
proxy.host,
|
|
proxy.port,
|
|
proxy.user,
|
|
proxy.password);
|
|
}
|
|
|
|
} // namespace MTP
|