diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index 306fb98864..4e25824b57 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -134,7 +134,9 @@ using ProxyData = MTP::ProxyData; ? "&pass=" + qthelp::url_encode(proxy.password) : "") + (((proxy.type == Type::Mtproto || proxy.type == Type::Web) && !proxy.password.isEmpty()) - ? "&secret=" + proxy.password : ""); + ? "&secret=" + ((proxy.type == Type::Web) + ? MTP::EncodeWebProxyLinkSecret(proxy) + : proxy.password) : ""); } [[nodiscard]] QString ProxyDataToLocalLink(const ProxyData &proxy) { @@ -347,6 +349,11 @@ void ShareProxy( } else if (type == ProxyData::Type::Mtproto || web) { proxy.password = fields.value(u"secret"_q); proxy.password.replace('+', '-').replace('/', '_'); + if (web) { + proxy.password = MTP::DecodeWebProxyLinkSecret( + proxy.password, + !proxy.webBasePath().isEmpty()); + } } return proxy; }; diff --git a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp index 7068d614af..f9748fcb63 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.cpp @@ -193,6 +193,15 @@ namespace { QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); } +// A base path needs a client that understands one, so a link that carries a +// path encodes its secret as base64url of this marker byte followed by the real +// secret. A client without path support decodes 17 bytes whose first byte is not +// the 0xDD of a padded secret, so it reports the link as an unsupported proxy +// type and asks the user to update, instead of accepting a pathless entry. +// 0xDD is the one byte that must never be used here: the older parser reads a +// 17-byte secret starting with it as an ordinary valid one. +constexpr auto kWebProxyLinkSecretMarker = uchar(0x70); + // 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. @@ -333,6 +342,12 @@ QString WebProxyBridgeCapability(const ProxyData &proxy) { == u"Proxy.Example.COM/App/"_q); Assert(StripWebProxyScheme(u"http://proxy.example.com"_q) == u"http://proxy.example.com"_q); + const auto plainSecret = u"8561944064fc730cbfa4473562d8ec59"_q; + const auto markedSecret = u"cIVhlEBk_HMMv6RHNWLY7Fk"_q; + Assert(DecodeWebProxyLinkSecret(markedSecret, true) == plainSecret); + Assert(DecodeWebProxyLinkSecret(markedSecret, false) == plainSecret); + Assert(DecodeWebProxyLinkSecret(plainSecret, false) == plainSecret); + Assert(DecodeWebProxyLinkSecret(plainSecret, true).isEmpty()); Assert(NormalizeWebProxyBasePath(u"per%20cent"_q).isEmpty()); Assert(NormalizeWebProxyBasePath(u"dot.ted"_q).isEmpty()); Assert(NormalizeWebProxyBasePath(u".."_q).isEmpty()); @@ -377,6 +392,43 @@ QString WebProxyBridgeCapability(const ProxyData &proxy) { key); } +QString EncodeWebProxyLinkSecret(const ProxyData &proxy) { + Expects(proxy.type == ProxyData::Type::Web); + + const auto secret = proxy.secretFromMtprotoPassword(); + if (proxy.webBasePath().isEmpty() || secret.empty()) { + return proxy.password; + } + auto marked = QByteArray(1, char(kWebProxyLinkSecretMarker)); + marked.append( + reinterpret_cast(secret.data()), + int(secret.size())); + return QString::fromLatin1(marked.toBase64( + QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); +} + +QString DecodeWebProxyLinkSecret(const QString &value, bool hasBasePath) { + const auto decoded = QByteArray::fromBase64( + value.toLatin1(), + QByteArray::Base64UrlEncoding + | QByteArray::AbortOnBase64DecodingErrors); + + // A canonical secret is 16 bytes, 17 starting with 0xDD, or 21+ starting + // with 0xEE, so a longer value behind this marker is never ambiguous. + const auto marked = (decoded.size() >= 17) + && (uchar(decoded[0]) == kWebProxyLinkSecretMarker); + if (marked) { + return QString::fromLatin1(decoded.mid(1).toHex()); + } + + // An unmarked secret on a link that carries a base path is rejected rather + // than accepted: that is exactly the link a client without path support + // would take for a pathless proxy on an empty host, so the marked form is + // required once a path is present. A root link keeps the plain secret and + // still works in those clients. + return hasBasePath ? QString() : value; +} + QString WebProxyBridgePath(const ProxyData &proxy) { Expects(proxy.type == ProxyData::Type::Web); diff --git a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.h b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.h index ecf0cef8aa..d8deebac23 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_proxy_data.h +++ b/Telegram/SourceFiles/mtproto/mtproto_proxy_data.h @@ -66,6 +66,10 @@ struct ProxyData { [[nodiscard]] QString NormalizeWebProxyHost(const QString &value); [[nodiscard]] QString WebProxyBridgeCapability(const ProxyData &proxy); +[[nodiscard]] QString EncodeWebProxyLinkSecret(const ProxyData &proxy); +[[nodiscard]] QString DecodeWebProxyLinkSecret( + const QString &value, + bool hasBasePath); [[nodiscard]] QString WebProxyBridgePath(const ProxyData &proxy); [[nodiscard]] QString WebProxyBridgeUrl(const ProxyData &proxy); [[nodiscard]] ProxyData ToDirectIpProxy( diff --git a/docs/web-proxy-plan.md b/docs/web-proxy-plan.md index cf6687f0bb..9c805e32d3 100644 --- a/docs/web-proxy-plan.md +++ b/docs/web-proxy-plan.md @@ -552,10 +552,14 @@ tg://webproxy?server=
&secret= `
` is the percent-encoded `host` or `host/path`, so a base path travels in the same parameter as `proxy.example.com%2Fdobry-cola-super-app`. There is no -separate path parameter. Following either link shows the address and secret with -one connect action. It does not check status or enable the proxy until that action -is invoked. Saved WEB entries can be shared as a public link or a direct-scheme QR -link. +separate path parameter. A link that carries a path also encodes its secret as +unpadded base64url of `0x70` plus the real secret, so a client without path +support reports an unsupported proxy type and asks the user to update, instead of +normalizing the address to an empty host and offering to connect to it. A +root link keeps the plain secret and still works in those clients. Following +either link shows the address and secret with one connect action. It does not +check status or enable the proxy until that action is invoked. Saved WEB entries +can be shared as a public link or a direct-scheme QR link. Application proxy changes configure/deconfigure the web transport before MTP sessions restart. WEB follows the MTProxy path in `Session`, `SessionPrivate`, and