mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
Support WEB proxies with /path.
This commit is contained in:
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QMessageAuthenticationCode>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QUrlQuery>
|
||||
#include <QtNetwork/QHostAddress>
|
||||
|
||||
namespace MTP {
|
||||
@@ -171,11 +172,19 @@ namespace {
|
||||
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 = QByteArray("tdesktop-web-proxy-bridge-v1\n")
|
||||
+ host.toLatin1();
|
||||
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,
|
||||
@@ -184,6 +193,66 @@ namespace {
|
||||
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) {
|
||||
@@ -226,6 +295,8 @@ QString NormalizeWebProxyHost(const QString &value) {
|
||||
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 = [] {
|
||||
@@ -245,16 +316,52 @@ QString WebProxyBridgeCapability(const ProxyData &proxy) {
|
||||
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
|
||||
@@ -264,7 +371,54 @@ QString WebProxyBridgeCapability(const ProxyData &proxy) {
|
||||
const auto key = QByteArray(
|
||||
reinterpret_cast<const char*>(secret.data()),
|
||||
int(secret.size()));
|
||||
return ComputeWebProxyBridgeCapability(proxy.host, key);
|
||||
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 {
|
||||
@@ -273,9 +427,10 @@ bool ProxyData::valid() const {
|
||||
|
||||
ProxyData::Status ProxyData::status() const {
|
||||
if (type == Type::Web) {
|
||||
if (host != NormalizeWebProxyHost(host)
|
||||
if (host.isEmpty()
|
||||
|| host != NormalizeWebProxyHost(host)
|
||||
|| port != 443
|
||||
|| !user.isEmpty()) {
|
||||
|| user != NormalizeWebProxyBasePath(user)) {
|
||||
return Status::Invalid;
|
||||
}
|
||||
const auto result = MtprotoPasswordStatus(password);
|
||||
|
||||
Reference in New Issue
Block a user