Support WEB proxies with /path.

This commit is contained in:
John Preston
2026-09-09 12:17:45 +04:00
parent efb596a02a
commit e3688099a1
8 changed files with 293 additions and 64 deletions

View File

@@ -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);