mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
Initial WEB proxy implementation.
This commit is contained in:
@@ -1599,6 +1599,8 @@ PRIVATE
|
||||
mtproto/core_types.h
|
||||
mtproto/dedicated_file_loader.cpp
|
||||
mtproto/dedicated_file_loader.h
|
||||
mtproto/details/mtproto_web_proxy_socket.cpp
|
||||
mtproto/details/mtproto_web_proxy_socket.h
|
||||
mtproto/facade.cpp
|
||||
mtproto/facade.h
|
||||
mtproto/mtp_instance.cpp
|
||||
@@ -1613,6 +1615,10 @@ PRIVATE
|
||||
mtproto/special_config_request.cpp
|
||||
mtproto/special_config_request.h
|
||||
mtproto/type_utils.h
|
||||
mtproto/web_proxy/web_proxy_frame.cpp
|
||||
mtproto/web_proxy/web_proxy_frame.h
|
||||
mtproto/web_proxy/web_proxy_transport.cpp
|
||||
mtproto/web_proxy/web_proxy_transport.h
|
||||
overview/overview_checkbox.cpp
|
||||
overview/overview_checkbox.h
|
||||
overview/overview_layout.cpp
|
||||
|
||||
@@ -1291,17 +1291,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_proxy_online" = "online";
|
||||
"lng_proxy_checking" = "checking…";
|
||||
"lng_proxy_connecting" = "connecting…";
|
||||
"lng_proxy_web_waiting" = "waiting for browser…";
|
||||
"lng_proxy_available" = "available (ping: {ping} ms)";
|
||||
"lng_proxy_unavailable" = "not available";
|
||||
"lng_proxy_edit" = "Edit proxy";
|
||||
"lng_proxy_menu_edit" = "Edit";
|
||||
"lng_proxy_menu_delete" = "Delete";
|
||||
"lng_proxy_menu_restore" = "Restore";
|
||||
"lng_proxy_web_open" = "Open browser";
|
||||
"lng_proxy_edit_share" = "Share";
|
||||
"lng_proxy_edit_share_qr_box_title" = "Share proxy with QR code";
|
||||
"lng_proxy_edit_share_list_button" = "Share Proxy List";
|
||||
"lng_proxy_edit_share_list_toast" = "Proxy List copied to clipboard.";
|
||||
"lng_proxy_address_label" = "Socket address";
|
||||
"lng_proxy_web_host_label" = "Web proxy hostname";
|
||||
"lng_proxy_web_host_ph" = "proxy.example.com";
|
||||
"lng_proxy_credentials_optional" = "Credentials (optional)";
|
||||
"lng_proxy_credentials" = "Credentials";
|
||||
"lng_proxy_description" = "Your saved proxy list will be here.";
|
||||
|
||||
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mtproto/facade.h"
|
||||
#include "mtproto/mtproto_config.h"
|
||||
#include "mtproto/proxy_check.h"
|
||||
#include "mtproto/web_proxy/web_proxy_transport.h"
|
||||
#include "qr/qr_generate.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "storage/localstorage.h"
|
||||
@@ -111,7 +112,8 @@ using ProxyData = MTP::ProxyData;
|
||||
case Type::Socks5: return u"socks"_q;
|
||||
case Type::Mtproto: return u"proxy"_q;
|
||||
case Type::None:
|
||||
case Type::Http: return QString();
|
||||
case Type::Http:
|
||||
case Type::Web: return QString();
|
||||
}
|
||||
Unexpected("Proxy type in ProxyDataToQueryPath.");
|
||||
}();
|
||||
@@ -567,6 +569,7 @@ public:
|
||||
rpl::producer<> editClicks() const;
|
||||
rpl::producer<> shareClicks() const;
|
||||
rpl::producer<> showQrClicks() const;
|
||||
rpl::producer<> openBrowserClicks() const;
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
@@ -589,6 +592,7 @@ private:
|
||||
rpl::event_stream<> _editClicks;
|
||||
rpl::event_stream<> _shareClicks;
|
||||
rpl::event_stream<> _showQrClicks;
|
||||
rpl::event_stream<> _openBrowserClicks;
|
||||
base::unique_qptr<Ui::DropdownMenu> _menu;
|
||||
|
||||
bool _set = false;
|
||||
@@ -663,7 +667,11 @@ private:
|
||||
|
||||
void prepare() override;
|
||||
void setInnerFocus() override {
|
||||
_host->setFocusFast();
|
||||
if (_type->current() == Type::Web) {
|
||||
_webHost->setFocusFast();
|
||||
} else {
|
||||
_host->setFocusFast();
|
||||
}
|
||||
}
|
||||
|
||||
void refreshButtons();
|
||||
@@ -673,6 +681,7 @@ private:
|
||||
void setupControls(const ProxyData &data);
|
||||
void setupTypes();
|
||||
void setupSocketAddress(const ProxyData &data);
|
||||
void setupWebAddress(const ProxyData &data);
|
||||
void setupCredentials(const ProxyData &data);
|
||||
void setupMtprotoCredentials(const ProxyData &data);
|
||||
|
||||
@@ -694,7 +703,10 @@ private:
|
||||
QPointer<Ui::InputField> _user;
|
||||
QPointer<Ui::PasswordInput> _password;
|
||||
QPointer<Base64UrlInput> _secret;
|
||||
QPointer<Ui::InputField> _webHost;
|
||||
|
||||
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _socketAddress;
|
||||
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _webAddress;
|
||||
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _credentials;
|
||||
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _mtprotoCredentials;
|
||||
|
||||
@@ -726,6 +738,10 @@ rpl::producer<> ProxyRow::showQrClicks() const {
|
||||
return _showQrClicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ProxyRow::openBrowserClicks() const {
|
||||
return _openBrowserClicks.events();
|
||||
}
|
||||
|
||||
void ProxyRow::setupControls(View &&view) {
|
||||
updateFields(std::move(view));
|
||||
_toggled.stop();
|
||||
@@ -747,7 +763,9 @@ void ProxyRow::updateFields(View &&view) {
|
||||
st::defaultRadio.duration);
|
||||
}
|
||||
_view = std::move(view);
|
||||
const auto endpoint = _view.host + ':' + QString::number(_view.port);
|
||||
const auto endpoint = _view.web
|
||||
? _view.host
|
||||
: _view.host + ':' + QString::number(_view.port);
|
||||
_title.setMarkedText(
|
||||
st::proxyRowTitleStyle,
|
||||
TextWithEntities()
|
||||
@@ -845,6 +863,8 @@ void ProxyRow::paintEvent(QPaintEvent *e) {
|
||||
return st::proxyRowStatusFgOnline;
|
||||
case State::Unavailable:
|
||||
return st::proxyRowStatusFgOffline;
|
||||
case State::WaitingForBrowser:
|
||||
return st::proxyRowStatusFg;
|
||||
case State::Available:
|
||||
return st::proxyRowStatusFgAvailable;
|
||||
default:
|
||||
@@ -866,6 +886,8 @@ void ProxyRow::paintEvent(QPaintEvent *e) {
|
||||
return tr::lng_proxy_online(tr::now);
|
||||
case State::Unavailable:
|
||||
return tr::lng_proxy_unavailable(tr::now);
|
||||
case State::WaitingForBrowser:
|
||||
return tr::lng_proxy_web_waiting(tr::now);
|
||||
}
|
||||
Unexpected("State in ProxyRow::paintEvent.");
|
||||
}();
|
||||
@@ -966,6 +988,11 @@ void ProxyRow::showMenu() {
|
||||
addAction(tr::lng_proxy_menu_edit(tr::now), [=] {
|
||||
_editClicks.fire({});
|
||||
}, &st::menuIconEdit);
|
||||
if (_view.canOpenBrowser) {
|
||||
addAction(tr::lng_proxy_web_open(tr::now), [=] {
|
||||
_openBrowserClicks.fire({});
|
||||
}, &st::menuIconLink);
|
||||
}
|
||||
if (_view.supportsShare) {
|
||||
addAction(tr::lng_proxy_edit_share(tr::now), [=] {
|
||||
_shareClicks.fire({});
|
||||
@@ -1403,6 +1430,11 @@ void ProxiesBox::setupButtons(int id, not_null<ProxyRow*> button) {
|
||||
_controller->shareItem(id, qr);
|
||||
}, button->lifetime());
|
||||
|
||||
button->openBrowserClicks(
|
||||
) | rpl::on_next([=] {
|
||||
_controller->openBrowser(id);
|
||||
}, button->lifetime());
|
||||
|
||||
button->clicks(
|
||||
) | rpl::on_next([=] {
|
||||
_controller->applyItem(id);
|
||||
@@ -1451,7 +1483,10 @@ void ProxyBox::prepare() {
|
||||
}, _port->lifetime());
|
||||
|
||||
const auto submit = [=] {
|
||||
if (_host->hasFocus()
|
||||
if (_webHost->hasFocus()
|
||||
&& !_webHost->getLastText().trimmed().isEmpty()) {
|
||||
_secret->setFocus();
|
||||
} else if (_host->hasFocus()
|
||||
&& !_host->getLastText().trimmed().isEmpty()) {
|
||||
_port->setFocus();
|
||||
} else if (_port->hasFocus()
|
||||
@@ -1471,6 +1506,8 @@ void ProxyBox::prepare() {
|
||||
connect(_port.data(), &Ui::MaskedInputField::submitted, submit);
|
||||
_user->submits(
|
||||
) | rpl::on_next(submit, _user->lifetime());
|
||||
_webHost->submits(
|
||||
) | rpl::on_next(submit, _webHost->lifetime());
|
||||
connect(_password.data(), &Ui::MaskedInputField::submitted, submit);
|
||||
connect(_secret.data(), &Ui::MaskedInputField::submitted, submit);
|
||||
|
||||
@@ -1506,25 +1543,38 @@ void ProxyBox::share() {
|
||||
ProxyData ProxyBox::collectData() {
|
||||
auto result = ProxyData();
|
||||
result.type = _type->current();
|
||||
result.host = _host->getLastText().trimmed();
|
||||
result.port = _port->getLastText().trimmed().toInt();
|
||||
result.user = (result.type == Type::Mtproto)
|
||||
const auto web = (result.type == Type::Web);
|
||||
result.host = web
|
||||
? MTP::NormalizeWebProxyHost(_webHost->getLastText())
|
||||
: _host->getLastText().trimmed();
|
||||
result.port = web
|
||||
? 443
|
||||
: _port->getLastText().trimmed().toInt();
|
||||
result.user = (result.type == Type::Mtproto || web)
|
||||
? QString()
|
||||
: _user->getLastText();
|
||||
result.password = (result.type == Type::Mtproto)
|
||||
result.password = (result.type == Type::Mtproto || web)
|
||||
? _secret->getLastText()
|
||||
: _password->getLastText();
|
||||
if (result.host.isEmpty()) {
|
||||
_host->showError();
|
||||
} else if (!result.port) {
|
||||
if (web) {
|
||||
_webHost->showError();
|
||||
} else {
|
||||
_host->showError();
|
||||
}
|
||||
} else if (!web && !result.port) {
|
||||
_port->showError();
|
||||
} else if ((result.type == Type::Http || result.type == Type::Socks5)
|
||||
&& !result.password.isEmpty() && result.user.isEmpty()) {
|
||||
_user->showError();
|
||||
} else if (result.type == Type::Mtproto && !result.valid()) {
|
||||
} else if ((result.type == Type::Mtproto || web) && !result.valid()) {
|
||||
_secret->showError();
|
||||
} else if (!result) {
|
||||
_host->showError();
|
||||
if (web) {
|
||||
_webHost->showError();
|
||||
} else {
|
||||
_host->showError();
|
||||
}
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
@@ -1533,6 +1583,7 @@ ProxyData ProxyBox::collectData() {
|
||||
|
||||
void ProxyBox::setupTypes() {
|
||||
const auto types = std::vector<std::pair<Type, QString>>{
|
||||
{ Type::Web, u"WEB"_q },
|
||||
{ Type::Mtproto, u"MTPROTO"_q },
|
||||
{ Type::Socks5, u"SOCKS5"_q },
|
||||
{ Type::Http, u"HTTP"_q },
|
||||
@@ -1558,10 +1609,15 @@ void ProxyBox::setupTypes() {
|
||||
}
|
||||
|
||||
void ProxyBox::setupSocketAddress(const ProxyData &data) {
|
||||
addLabel(_content, tr::lng_proxy_address_label(tr::now));
|
||||
const auto address = _content->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
_socketAddress = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_content,
|
||||
object_ptr<Ui::VerticalLayout>(_content)));
|
||||
const auto content = _socketAddress->entity();
|
||||
addLabel(content, tr::lng_proxy_address_label(tr::now));
|
||||
const auto address = content->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
content,
|
||||
st::connectionHostInputField.heightMin),
|
||||
st::proxyEditInputPadding);
|
||||
_host = Ui::CreateChild<HostInput>(
|
||||
@@ -1585,6 +1641,22 @@ void ProxyBox::setupSocketAddress(const ProxyData &data) {
|
||||
}, address->lifetime());
|
||||
}
|
||||
|
||||
void ProxyBox::setupWebAddress(const ProxyData &data) {
|
||||
_webAddress = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
_content,
|
||||
object_ptr<Ui::VerticalLayout>(_content)));
|
||||
const auto content = _webAddress->entity();
|
||||
addLabel(content, tr::lng_proxy_web_host_label(tr::now));
|
||||
_webHost = content->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
content,
|
||||
st::connectionUserInputField,
|
||||
tr::lng_proxy_web_host_ph(),
|
||||
(data.type == Type::Web) ? data.host : QString()),
|
||||
st::proxyEditInputPadding);
|
||||
}
|
||||
|
||||
void ProxyBox::setupCredentials(const ProxyData &data) {
|
||||
_credentials = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
@@ -1605,7 +1677,9 @@ void ProxyBox::setupCredentials(const ProxyData &data) {
|
||||
passwordWrap.data(),
|
||||
st::connectionPasswordInputField,
|
||||
tr::lng_connection_password_ph(),
|
||||
(data.type == Type::Mtproto) ? QString() : data.password);
|
||||
(data.type == Type::Mtproto || data.type == Type::Web)
|
||||
? QString()
|
||||
: data.password);
|
||||
_password->move(0, 0);
|
||||
_password->heightValue(
|
||||
) | rpl::on_next([=, wrap = passwordWrap.data()](int height) {
|
||||
@@ -1631,7 +1705,9 @@ void ProxyBox::setupMtprotoCredentials(const ProxyData &data) {
|
||||
secretWrap.data(),
|
||||
st::connectionUserInputField,
|
||||
tr::lng_connection_proxy_secret_ph(),
|
||||
(data.type == Type::Mtproto) ? data.password : QString());
|
||||
(data.type == Type::Mtproto || data.type == Type::Web)
|
||||
? data.password
|
||||
: QString());
|
||||
_secret->move(0, 0);
|
||||
_secret->heightValue(
|
||||
) | rpl::on_next([=, wrap = secretWrap.data()](int height) {
|
||||
@@ -1655,13 +1731,17 @@ void ProxyBox::setupControls(const ProxyData &data) {
|
||||
|
||||
setupTypes();
|
||||
setupSocketAddress(data);
|
||||
setupWebAddress(data);
|
||||
setupCredentials(data);
|
||||
setupMtprotoCredentials(data);
|
||||
|
||||
const auto handleType = [=](Type type) {
|
||||
const auto web = (type == Type::Web);
|
||||
const auto credentialsShown
|
||||
= (type == Type::Http || type == Type::Socks5);
|
||||
const auto mtprotoShown = (type == Type::Mtproto);
|
||||
const auto mtprotoShown = (type == Type::Mtproto || web);
|
||||
_socketAddress->toggle(!web, anim::type::instant);
|
||||
_webAddress->toggle(web, anim::type::instant);
|
||||
_credentials->toggle(credentialsShown, anim::type::instant);
|
||||
_mtprotoCredentials->toggle(mtprotoShown, anim::type::instant);
|
||||
_aboutSponsored->toggle(mtprotoShown, anim::type::instant);
|
||||
@@ -1672,6 +1752,7 @@ void ProxyBox::setupControls(const ProxyData &data) {
|
||||
_password->setFocusPolicy(credentialsPolicy);
|
||||
_secret->setFocusPolicy(
|
||||
mtprotoShown ? Qt::StrongFocus : Qt::NoFocus);
|
||||
_webHost->setFocusPolicy(web ? Qt::StrongFocus : Qt::NoFocus);
|
||||
};
|
||||
_type->setChangedCallback([=](Type type) {
|
||||
handleType(type);
|
||||
@@ -1715,6 +1796,14 @@ ProxiesBoxController::ProxiesBoxController(not_null<Main::Account*> account)
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
MTP::WebProxy::Transport::StateChanges(
|
||||
) | rpl::on_next([=](const MTP::WebProxy::Transport::StateChange &change) {
|
||||
const auto i = findByProxy(change.proxy);
|
||||
if (i != end(_list)) {
|
||||
updateView(*i);
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
for (auto &item : _list) {
|
||||
refreshChecker(item);
|
||||
}
|
||||
@@ -2011,6 +2100,13 @@ auto ProxiesBoxController::proxySettingsValue() const
|
||||
}
|
||||
|
||||
void ProxiesBoxController::refreshChecker(Item &item) {
|
||||
if (item.data.type == Type::Web) {
|
||||
MTP::ResetProxyCheckers(item.checker, item.checkerv6);
|
||||
item.state = ItemState::Unavailable;
|
||||
item.ping = 0;
|
||||
updateView(item);
|
||||
return;
|
||||
}
|
||||
item.state = ItemState::Checking;
|
||||
const auto id = item.id;
|
||||
MTP::StartProxyCheck(
|
||||
@@ -2151,6 +2247,15 @@ void ProxiesBoxController::applyItem(int id) {
|
||||
updateView(*item);
|
||||
}
|
||||
|
||||
void ProxiesBoxController::openBrowser(int id) {
|
||||
const auto item = findById(id);
|
||||
if (_settings.isEnabled()
|
||||
&& _settings.selected() == item->data
|
||||
&& item->data.type == Type::Web) {
|
||||
MTP::WebProxy::Transport::OpenBrowser(item->data);
|
||||
}
|
||||
}
|
||||
|
||||
void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
||||
auto item = findById(id);
|
||||
item->deleted = deleted;
|
||||
@@ -2373,12 +2478,26 @@ void ProxiesBoxController::updateView(const Item &item) {
|
||||
case Type::Http: return u"HTTP"_q;
|
||||
case Type::Socks5: return u"SOCKS5"_q;
|
||||
case Type::Mtproto: return u"MTPROTO"_q;
|
||||
case Type::Web: return u"WEB"_q;
|
||||
}
|
||||
Unexpected("Proxy type in ProxiesBoxController::updateView.");
|
||||
}();
|
||||
const auto state = [&] {
|
||||
if (!selected || !_settings.isEnabled()) {
|
||||
return item.state;
|
||||
} else if (item.data.type == Type::Web) {
|
||||
switch (MTP::WebProxy::Transport::CurrentState(item.data)) {
|
||||
case MTP::WebProxy::Transport::State::WaitingForBrowser:
|
||||
return ItemState::WaitingForBrowser;
|
||||
case MTP::WebProxy::Transport::State::Failed:
|
||||
return ItemState::Unavailable;
|
||||
case MTP::WebProxy::Transport::State::Connected:
|
||||
return ItemState::Online;
|
||||
case MTP::WebProxy::Transport::State::Idle:
|
||||
case MTP::WebProxy::Transport::State::Connecting:
|
||||
return ItemState::Connecting;
|
||||
}
|
||||
Unexpected("Web proxy transport state.");
|
||||
} else if (_account->mtp().dcstate() == MTP::ConnectedState) {
|
||||
return ItemState::Online;
|
||||
}
|
||||
@@ -2396,6 +2515,11 @@ void ProxiesBoxController::updateView(const Item &item) {
|
||||
deleted,
|
||||
!deleted && supportsShare,
|
||||
supportsCalls,
|
||||
item.data.type == Type::Web,
|
||||
!deleted
|
||||
&& selected
|
||||
&& _settings.isEnabled()
|
||||
&& item.data.type == Type::Web,
|
||||
state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ public:
|
||||
Online,
|
||||
Checking,
|
||||
Available,
|
||||
Unavailable
|
||||
Unavailable,
|
||||
WaitingForBrowser,
|
||||
};
|
||||
struct ItemView {
|
||||
int id = 0;
|
||||
@@ -73,6 +74,8 @@ public:
|
||||
bool deleted = false;
|
||||
bool supportsShare = false;
|
||||
bool supportsCalls = false;
|
||||
bool web = false;
|
||||
bool canOpenBrowser = false;
|
||||
ItemState state = ItemState::Checking;
|
||||
|
||||
};
|
||||
@@ -83,6 +86,7 @@ public:
|
||||
void shareItem(int id, bool qr);
|
||||
void shareItems();
|
||||
void applyItem(int id);
|
||||
void openBrowser(int id);
|
||||
object_ptr<Ui::BoxContent> editItemBox(int id);
|
||||
object_ptr<Ui::BoxContent> addNewItemBox();
|
||||
bool setProxySettings(ProxyData::Settings value);
|
||||
|
||||
@@ -64,6 +64,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "media/view/media_view_open_common.h"
|
||||
#include "mtproto/mtproto_dc_options.h"
|
||||
#include "mtproto/mtproto_config.h"
|
||||
#include "mtproto/web_proxy/web_proxy_transport.h"
|
||||
#include "media/audio/media_audio_track.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "media/player/media_player_float.h"
|
||||
@@ -247,6 +248,7 @@ Application::~Application() {
|
||||
|
||||
_private->proxyRotation = nullptr;
|
||||
_domain->finish();
|
||||
MTP::WebProxy::Transport::Shutdown();
|
||||
|
||||
Local::finish();
|
||||
|
||||
@@ -1871,6 +1873,15 @@ void Application::postponeCall(FnMut<void()> &&callable) {
|
||||
}
|
||||
|
||||
void Application::refreshGlobalProxy() {
|
||||
const auto &proxySettings = settings().proxy();
|
||||
const auto proxy = proxySettings.isEnabled()
|
||||
? proxySettings.selected()
|
||||
: MTP::ProxyData();
|
||||
if (proxy.type == MTP::ProxyData::Type::Web && proxy.valid()) {
|
||||
MTP::WebProxy::Transport::Activate(proxy);
|
||||
} else {
|
||||
MTP::WebProxy::Transport::Deactivate();
|
||||
}
|
||||
Sandbox::Instance().refreshGlobalProxy();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ namespace {
|
||||
case 1: return MTP::ProxyData::Type::Socks5;
|
||||
case 2: return MTP::ProxyData::Type::Http;
|
||||
case 3: return MTP::ProxyData::Type::Mtproto;
|
||||
case 4: return MTP::ProxyData::Type::Web;
|
||||
}
|
||||
Unexpected("Bad type in DeserializeProxyData");
|
||||
return MTP::ProxyData::Type::None;
|
||||
}();
|
||||
return proxy;
|
||||
}
|
||||
@@ -74,6 +75,7 @@ namespace {
|
||||
case MTP::ProxyData::Type::Socks5: return 1;
|
||||
case MTP::ProxyData::Type::Http: return 2;
|
||||
case MTP::ProxyData::Type::Mtproto: return 3;
|
||||
case MTP::ProxyData::Type::Web: return 4;
|
||||
}
|
||||
Unexpected("Bad type in SerializeProxyData");
|
||||
}();
|
||||
|
||||
@@ -153,16 +153,23 @@ void ProxyRotationManager::pruneRemovedEntries() {
|
||||
void ProxyRotationManager::updateProbeOrder() {
|
||||
const auto &settings = App().settings().proxy();
|
||||
const auto currentIndex = settings.indexInList(settings.selected());
|
||||
const auto canCheck = [&](int index) {
|
||||
return index >= 0
|
||||
&& index < int(settings.list().size())
|
||||
&& settings.list()[index].type != MTP::ProxyData::Type::Web;
|
||||
};
|
||||
_probeOrder.clear();
|
||||
_probeOrder.reserve(settings.list().size());
|
||||
for (const auto index : settings.proxyRotationPreferredIndices()) {
|
||||
if (index == currentIndex) {
|
||||
if (index == currentIndex || !canCheck(index)) {
|
||||
continue;
|
||||
}
|
||||
_probeOrder.push_back(index);
|
||||
}
|
||||
for (auto i = 0, count = int(settings.list().size()); i != count; ++i) {
|
||||
if (i == currentIndex || ranges::contains(_probeOrder, i)) {
|
||||
if (i == currentIndex
|
||||
|| !canCheck(i)
|
||||
|| ranges::contains(_probeOrder, i)) {
|
||||
continue;
|
||||
}
|
||||
_probeOrder.push_back(i);
|
||||
|
||||
@@ -76,7 +76,8 @@ void PromoSuggestions::refreshTopPromotion() {
|
||||
return {};
|
||||
}
|
||||
const auto &proxy = Core::App().settings().proxy().selected();
|
||||
if (proxy.type != MTP::ProxyData::Type::Mtproto) {
|
||||
if (proxy.type != MTP::ProxyData::Type::Mtproto
|
||||
&& proxy.type != MTP::ProxyData::Type::Web) {
|
||||
return {};
|
||||
}
|
||||
return { proxy.host, proxy.port };
|
||||
|
||||
@@ -97,7 +97,8 @@ void Account::watchProxyChanges() {
|
||||
Core::App().proxyChanges(
|
||||
) | rpl::on_next([=](const ProxyChange &change) {
|
||||
const auto key = [&](const MTP::ProxyData &proxy) {
|
||||
return (proxy.type == MTP::ProxyData::Type::Mtproto)
|
||||
return (proxy.type == MTP::ProxyData::Type::Mtproto
|
||||
|| proxy.type == MTP::ProxyData::Type::Web)
|
||||
? std::make_pair(proxy.host, proxy.port)
|
||||
: std::make_pair(QString(), uint32(0));
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mtproto/connection_tcp.h"
|
||||
|
||||
#include "mtproto/details/mtproto_abstract_socket.h"
|
||||
#include "mtproto/details/mtproto_web_proxy_socket.h"
|
||||
#include "base/bytes.h"
|
||||
#include "base/openssl_help.h"
|
||||
#include "base/random.h"
|
||||
@@ -514,10 +515,12 @@ void TcpConnection::connectToServer(
|
||||
Expects(_protocol == nullptr);
|
||||
Expects(_protocolDcId == 0);
|
||||
|
||||
const auto secret = (_proxy.type == ProxyData::Type::Mtproto)
|
||||
const auto proxyProtocol = (_proxy.type == ProxyData::Type::Mtproto)
|
||||
|| (_proxy.type == ProxyData::Type::Web);
|
||||
const auto secret = proxyProtocol
|
||||
? _proxy.secretFromMtprotoPassword()
|
||||
: protocolSecret;
|
||||
if (_proxy.type == ProxyData::Type::Mtproto) {
|
||||
if (proxyProtocol) {
|
||||
_address = _proxy.host;
|
||||
_port = _proxy.port;
|
||||
_protocol = Protocol::Create(secret);
|
||||
@@ -526,11 +529,13 @@ void TcpConnection::connectToServer(
|
||||
_port = port;
|
||||
_protocol = Protocol::Create(secret);
|
||||
}
|
||||
_socket = AbstractSocket::Create(
|
||||
thread(),
|
||||
secret,
|
||||
ToNetworkProxy(_proxy),
|
||||
protocolForFiles);
|
||||
_socket = (_proxy.type == ProxyData::Type::Web)
|
||||
? std::make_unique<WebProxySocket>(thread(), _proxy)
|
||||
: AbstractSocket::Create(
|
||||
thread(),
|
||||
secret,
|
||||
ToNetworkProxy(_proxy),
|
||||
protocolForFiles);
|
||||
_protocolDcId = protocolDcId;
|
||||
|
||||
const auto postfix = _socket->debugPostfix();
|
||||
@@ -538,7 +543,11 @@ void TcpConnection::connectToServer(
|
||||
.arg(_debugId.toInt())
|
||||
.arg(
|
||||
ProtocolDcDebugId(_protocolDcId),
|
||||
(_proxy.type == ProxyData::Type::Mtproto) ? "mtproxy " : "",
|
||||
(_proxy.type == ProxyData::Type::Mtproto)
|
||||
? "mtproxy "
|
||||
: (_proxy.type == ProxyData::Type::Web)
|
||||
? "webproxy "
|
||||
: "",
|
||||
_address)
|
||||
.arg(_port)
|
||||
.arg(postfix.isEmpty() ? _protocol->debugPostfix() : postfix);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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/details/mtproto_web_proxy_socket.h"
|
||||
|
||||
#include "mtproto/web_proxy/web_proxy_transport.h"
|
||||
|
||||
namespace MTP::details {
|
||||
|
||||
WebProxySocket::WebProxySocket(
|
||||
not_null<QThread*> thread,
|
||||
const ProxyData &proxy)
|
||||
: AbstractSocket(thread)
|
||||
, _streamId(WebProxy::Transport::NextStreamId())
|
||||
, _transport(WebProxy::Transport::Instance()) {
|
||||
Expects(proxy.type == ProxyData::Type::Web);
|
||||
}
|
||||
|
||||
WebProxySocket::~WebProxySocket() {
|
||||
if (_transport) {
|
||||
_transport->closeStream(_streamId);
|
||||
}
|
||||
}
|
||||
|
||||
void WebProxySocket::connectToHost(const QString &, int) {
|
||||
Expects(_state == State::NotConnected);
|
||||
|
||||
_state = State::Connecting;
|
||||
if (_transport) {
|
||||
_transport->registerStream(_streamId, {
|
||||
.context = this,
|
||||
.connected = [=] { transportConnected(); },
|
||||
.data = [=](QByteArray data) {
|
||||
transportData(std::move(data));
|
||||
},
|
||||
.disconnected = [=] { transportDisconnected(); },
|
||||
.failed = [=] { transportFailed(); },
|
||||
});
|
||||
} else {
|
||||
transportFailed();
|
||||
}
|
||||
}
|
||||
|
||||
bool WebProxySocket::isGoodStartNonce(bytes::const_span nonce) {
|
||||
Expects(nonce.size() >= 2 * sizeof(uint32));
|
||||
|
||||
const auto bytes = nonce.data();
|
||||
const auto zero = *reinterpret_cast<const uchar*>(bytes);
|
||||
const auto first = *reinterpret_cast<const uint32*>(bytes);
|
||||
const auto second = *(reinterpret_cast<const uint32*>(bytes) + 1);
|
||||
return (zero != 0xEFU)
|
||||
&& (first != 0x44414548U)
|
||||
&& (first != 0x54534F50U)
|
||||
&& (first != 0x20544547U)
|
||||
&& (first != 0xEEEEEEEEU)
|
||||
&& (first != 0xDDDDDDDDU)
|
||||
&& (first != 0x02010316U)
|
||||
&& (second != 0x00000000U);
|
||||
}
|
||||
|
||||
void WebProxySocket::timedOut() {
|
||||
}
|
||||
|
||||
bool WebProxySocket::isConnected() {
|
||||
return (_state == State::Connected);
|
||||
}
|
||||
|
||||
bool WebProxySocket::hasBytesAvailable() {
|
||||
return (_incomingOffset < _incoming.size());
|
||||
}
|
||||
|
||||
int64 WebProxySocket::read(bytes::span buffer) {
|
||||
const auto available = _incoming.size() - _incomingOffset;
|
||||
if (available <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const auto count = int(std::min<std::size_t>(available, buffer.size()));
|
||||
if (!count) {
|
||||
return 0;
|
||||
}
|
||||
bytes::copy(
|
||||
buffer,
|
||||
bytes::make_span(_incoming).subspan(_incomingOffset, count));
|
||||
_incomingOffset += count;
|
||||
if (_incomingOffset == _incoming.size()) {
|
||||
_incoming.clear();
|
||||
_incomingOffset = 0;
|
||||
} else if (_incomingOffset >= 64 * 1024
|
||||
&& _incomingOffset >= _incoming.size() / 2) {
|
||||
_incoming.remove(0, _incomingOffset);
|
||||
_incomingOffset = 0;
|
||||
}
|
||||
if (_transport) {
|
||||
_transport->grantWindow(_streamId, count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void WebProxySocket::write(
|
||||
bytes::const_span prefix,
|
||||
bytes::const_span buffer) {
|
||||
Expects(!buffer.empty());
|
||||
|
||||
if (!isConnected() || !_transport) {
|
||||
return;
|
||||
}
|
||||
auto data = QByteArray(
|
||||
int(prefix.size() + buffer.size()),
|
||||
Qt::Uninitialized);
|
||||
if (!prefix.empty()) {
|
||||
memcpy(data.data(), prefix.data(), prefix.size());
|
||||
}
|
||||
memcpy(data.data() + prefix.size(), buffer.data(), buffer.size());
|
||||
_transport->sendData(_streamId, std::move(data));
|
||||
}
|
||||
|
||||
int32 WebProxySocket::debugState() {
|
||||
return int32(_state);
|
||||
}
|
||||
|
||||
QString WebProxySocket::debugPostfix() const {
|
||||
return u"_web"_q;
|
||||
}
|
||||
|
||||
void WebProxySocket::transportConnected() {
|
||||
if (_state != State::Connecting) {
|
||||
return;
|
||||
}
|
||||
_state = State::Connected;
|
||||
_connected.fire({});
|
||||
}
|
||||
|
||||
void WebProxySocket::transportData(QByteArray data) {
|
||||
if (_state != State::Connected || data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_incoming.append(data);
|
||||
_readyRead.fire({});
|
||||
}
|
||||
|
||||
void WebProxySocket::transportDisconnected() {
|
||||
if (_state == State::Disconnected || _state == State::Error) {
|
||||
return;
|
||||
}
|
||||
_state = State::Disconnected;
|
||||
_disconnected.fire({});
|
||||
}
|
||||
|
||||
void WebProxySocket::transportFailed() {
|
||||
if (_state == State::Disconnected || _state == State::Error) {
|
||||
return;
|
||||
}
|
||||
_state = State::Error;
|
||||
_error.fire({});
|
||||
}
|
||||
|
||||
} // namespace MTP::details
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "mtproto/details/mtproto_abstract_socket.h"
|
||||
#include "mtproto/mtproto_proxy_data.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
namespace MTP::WebProxy {
|
||||
class Transport;
|
||||
} // namespace MTP::WebProxy
|
||||
|
||||
namespace MTP::details {
|
||||
|
||||
class WebProxySocket final : public AbstractSocket {
|
||||
public:
|
||||
WebProxySocket(
|
||||
not_null<QThread*> thread,
|
||||
const ProxyData &proxy);
|
||||
~WebProxySocket();
|
||||
|
||||
void connectToHost(const QString &address, int port) override;
|
||||
bool isGoodStartNonce(bytes::const_span nonce) override;
|
||||
void timedOut() override;
|
||||
bool isConnected() override;
|
||||
bool hasBytesAvailable() override;
|
||||
int64 read(bytes::span buffer) override;
|
||||
void write(bytes::const_span prefix, bytes::const_span buffer) override;
|
||||
|
||||
int32 debugState() override;
|
||||
QString debugPostfix() const override;
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
NotConnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
Error,
|
||||
};
|
||||
|
||||
void transportConnected();
|
||||
void transportData(QByteArray data);
|
||||
void transportDisconnected();
|
||||
void transportFailed();
|
||||
|
||||
const uint32 _streamId;
|
||||
WebProxy::Transport *_transport = nullptr;
|
||||
QByteArray _incoming;
|
||||
int _incomingOffset = 0;
|
||||
State _state = State::NotConnected;
|
||||
|
||||
};
|
||||
|
||||
} // namespace MTP::details
|
||||
@@ -10,6 +10,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/qthelp_url.h"
|
||||
#include "base/qt/qt_string_view.h"
|
||||
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QMessageAuthenticationCode>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtNetwork/QHostAddress>
|
||||
|
||||
namespace MTP {
|
||||
namespace {
|
||||
|
||||
@@ -143,14 +148,112 @@ namespace {
|
||||
return bytes::make_vector(bytes::make_span(result));
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ComputeWebProxyBridgeCapability(
|
||||
const QString &host,
|
||||
const QByteArray &key) {
|
||||
const auto context = QByteArray("tdesktop-web-proxy-bridge-v1\n")
|
||||
+ host.toLatin1();
|
||||
return QString::fromLatin1(QMessageAuthenticationCode::hash(
|
||||
context,
|
||||
key,
|
||||
QCryptographicHash::Sha256
|
||||
).toBase64(
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||
}
|
||||
|
||||
} // 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
|
||||
#ifndef NDEBUG
|
||||
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"localhost"_q).isEmpty());
|
||||
Assert(NormalizeWebProxyHost(u"127.0.0.1"_q).isEmpty());
|
||||
Assert(NormalizeWebProxyHost(u"site.example:443"_q).isEmpty());
|
||||
Assert(NormalizeWebProxyHost(u"site..example"_q).isEmpty());
|
||||
const auto plain = QByteArray::fromHex(
|
||||
"000102030405060708090a0b0c0d0e0f");
|
||||
const auto padded = QByteArray::fromHex(
|
||||
"dd000102030405060708090a0b0c0d0e0f");
|
||||
Assert(ComputeWebProxyBridgeCapability(
|
||||
u"proxy.example.com"_q,
|
||||
plain) == u"MHLEY5PmW1GWqJkSrlmJpvJUiLhBH_QKy6yKg8a0JPk"_q);
|
||||
Assert(ComputeWebProxyBridgeCapability(
|
||||
u"proxy.example.com"_q,
|
||||
padded) == u"IpJrt3e7sKtzPyoXy6w-Zj6GGEvsvclN66JzQEfPYLA"_q);
|
||||
return true;
|
||||
}();
|
||||
(void)checked;
|
||||
#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, key);
|
||||
}
|
||||
|
||||
bool ProxyData::valid() const {
|
||||
return status() == Status::Valid;
|
||||
}
|
||||
|
||||
ProxyData::Status ProxyData::status() const {
|
||||
if (type == Type::None || host.isEmpty() || !port) {
|
||||
if (type == Type::Web) {
|
||||
if (host != NormalizeWebProxyHost(host)
|
||||
|| port != 443
|
||||
|| !user.isEmpty()) {
|
||||
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);
|
||||
@@ -172,7 +275,7 @@ bool ProxyData::tryCustomResolve() const {
|
||||
}
|
||||
|
||||
bytes::vector ProxyData::secretFromMtprotoPassword() const {
|
||||
Expects(type == Type::Mtproto);
|
||||
Expects(type == Type::Mtproto || type == Type::Web);
|
||||
|
||||
if (IsHexMtprotoPassword(password)) {
|
||||
return SecretFromHexMtprotoPassword(password);
|
||||
@@ -232,7 +335,8 @@ ProxyData ToDirectIpProxy(const ProxyData &proxy, int ipIndex) {
|
||||
QNetworkProxy ToNetworkProxy(const ProxyData &proxy) {
|
||||
if (proxy.type == ProxyData::Type::None) {
|
||||
return QNetworkProxy::DefaultProxy;
|
||||
} else if (proxy.type == ProxyData::Type::Mtproto) {
|
||||
} else if (proxy.type == ProxyData::Type::Mtproto
|
||||
|| proxy.type == ProxyData::Type::Web) {
|
||||
return QNetworkProxy::NoProxy;
|
||||
}
|
||||
return QNetworkProxy(
|
||||
|
||||
@@ -22,6 +22,7 @@ struct ProxyData {
|
||||
Socks5,
|
||||
Http,
|
||||
Mtproto,
|
||||
Web,
|
||||
};
|
||||
enum class Status {
|
||||
Valid,
|
||||
@@ -53,6 +54,8 @@ struct ProxyData {
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString NormalizeWebProxyHost(const QString &value);
|
||||
[[nodiscard]] QString WebProxyBridgeCapability(const ProxyData &proxy);
|
||||
[[nodiscard]] ProxyData ToDirectIpProxy(
|
||||
const ProxyData &proxy,
|
||||
int ipIndex = 0);
|
||||
|
||||
@@ -49,6 +49,9 @@ void StartProxyCheck(
|
||||
using Variants = DcOptions::Variants;
|
||||
|
||||
ResetProxyCheckers(v4, v6);
|
||||
if (proxy.type == ProxyData::Type::Web) {
|
||||
return;
|
||||
}
|
||||
const auto connType = (proxy.type == ProxyData::Type::Http)
|
||||
? Variants::Http
|
||||
: Variants::Tcp;
|
||||
|
||||
@@ -242,7 +242,8 @@ void Session::refreshOptions() {
|
||||
const auto isEnabled = settings.isEnabled();
|
||||
const auto proxyType = (isEnabled ? proxy.type : ProxyData::Type::None);
|
||||
const auto useTcp = (proxyType != ProxyData::Type::Http);
|
||||
const auto useHttp = (proxyType != ProxyData::Type::Mtproto);
|
||||
const auto useHttp = (proxyType != ProxyData::Type::Mtproto)
|
||||
&& (proxyType != ProxyData::Type::Web);
|
||||
const auto useIPv4 = true;
|
||||
const auto useIPv6 = settings.tryIPv6();
|
||||
_data->setOptions(SessionOptions(
|
||||
|
||||
@@ -687,7 +687,8 @@ void SessionPrivate::tryToSend() {
|
||||
: _instance->systemVersion();
|
||||
const auto appVersion = ComputeAppVersion();
|
||||
const auto proxyType = _options->proxy.type;
|
||||
const auto mtprotoProxy = (proxyType == ProxyData::Type::Mtproto);
|
||||
const auto mtprotoProxy = (proxyType == ProxyData::Type::Mtproto)
|
||||
|| (proxyType == ProxyData::Type::Web);
|
||||
const auto clientProxyFields = mtprotoProxy
|
||||
? MTP_inputClientProxy(
|
||||
MTP_string(_options->proxy.host),
|
||||
@@ -1034,7 +1035,8 @@ void SessionPrivate::connectToServer(bool afterConfig) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_options->proxy.type == ProxyData::Type::Mtproto) {
|
||||
if (_options->proxy.type == ProxyData::Type::Mtproto
|
||||
|| _options->proxy.type == ProxyData::Type::Web) {
|
||||
// host, port, secret for mtproto proxy are taken from proxy.
|
||||
appendTestConnection(DcOptions::Variants::Tcp, {}, 0, {});
|
||||
} else {
|
||||
|
||||
94
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_frame.cpp
Normal file
94
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_frame.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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/web_proxy/web_proxy_frame.h"
|
||||
|
||||
namespace MTP::WebProxy {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] bool IsKnownType(uchar value) {
|
||||
switch (FrameType(value)) {
|
||||
case FrameType::Open:
|
||||
case FrameType::Data:
|
||||
case FrameType::Close:
|
||||
case FrameType::Window:
|
||||
case FrameType::Ping:
|
||||
case FrameType::Pong:
|
||||
case FrameType::Hello:
|
||||
case FrameType::Welcome:
|
||||
case FrameType::AuthChallenge:
|
||||
case FrameType::AuthResponse:
|
||||
case FrameType::Bye:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QByteArray SerializeFrame(
|
||||
FrameType type,
|
||||
uint32 streamId,
|
||||
const QByteArray &payload) {
|
||||
Expects(streamId <= 0x00FFFFFF);
|
||||
Expects(payload.size() <= kMaxFramePayload);
|
||||
|
||||
auto result = QByteArray(kFrameHeaderSize + payload.size(), Qt::Uninitialized);
|
||||
auto data = reinterpret_cast<uchar*>(result.data());
|
||||
data[0] = uchar(type);
|
||||
data[1] = uchar(streamId >> 16);
|
||||
data[2] = uchar(streamId >> 8);
|
||||
data[3] = uchar(streamId);
|
||||
const auto size = uint32(payload.size());
|
||||
data[4] = uchar(size >> 24);
|
||||
data[5] = uchar(size >> 16);
|
||||
data[6] = uchar(size >> 8);
|
||||
data[7] = uchar(size);
|
||||
if (!payload.isEmpty()) {
|
||||
memcpy(result.data() + kFrameHeaderSize, payload.constData(), payload.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ParseFrames(QByteArray &buffer, std::vector<Frame> &output) {
|
||||
auto offset = 0;
|
||||
while (buffer.size() - offset >= kFrameHeaderSize) {
|
||||
const auto data = reinterpret_cast<const uchar*>(buffer.constData() + offset);
|
||||
if (!IsKnownType(data[0])) {
|
||||
return false;
|
||||
}
|
||||
const auto streamId = (uint32(data[1]) << 16)
|
||||
| (uint32(data[2]) << 8)
|
||||
| uint32(data[3]);
|
||||
const auto size = (uint32(data[4]) << 24)
|
||||
| (uint32(data[5]) << 16)
|
||||
| (uint32(data[6]) << 8)
|
||||
| uint32(data[7]);
|
||||
if (size > kMaxFramePayload) {
|
||||
return false;
|
||||
}
|
||||
const auto full = kFrameHeaderSize + int(size);
|
||||
if (buffer.size() - offset < full) {
|
||||
break;
|
||||
}
|
||||
if (output.size() >= std::size_t(kMaxBatchFrames)) {
|
||||
return false;
|
||||
}
|
||||
output.push_back({
|
||||
FrameType(data[0]),
|
||||
streamId,
|
||||
buffer.mid(offset + kFrameHeaderSize, size),
|
||||
});
|
||||
offset += full;
|
||||
}
|
||||
if (offset > 0) {
|
||||
buffer.remove(0, offset);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MTP::WebProxy
|
||||
51
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_frame.h
Normal file
51
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_frame.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/basic_types.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MTP::WebProxy {
|
||||
|
||||
inline constexpr auto kFrameHeaderSize = 8;
|
||||
inline constexpr auto kMaxFramePayload = 1024 * 1024;
|
||||
inline constexpr auto kMaxBatchFrames = 4096;
|
||||
inline constexpr auto kInitialStreamWindow = 4 * 1024 * 1024;
|
||||
|
||||
enum class FrameType : uchar {
|
||||
Open = 0x01,
|
||||
Data = 0x02,
|
||||
Close = 0x03,
|
||||
Window = 0x04,
|
||||
Ping = 0x05,
|
||||
Pong = 0x06,
|
||||
Hello = 0x10,
|
||||
Welcome = 0x11,
|
||||
AuthChallenge = 0x12,
|
||||
AuthResponse = 0x13,
|
||||
Bye = 0x1F,
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
FrameType type = FrameType::Bye;
|
||||
uint32 streamId = 0;
|
||||
QByteArray payload;
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray SerializeFrame(
|
||||
FrameType type,
|
||||
uint32 streamId,
|
||||
const QByteArray &payload = QByteArray());
|
||||
[[nodiscard]] bool ParseFrames(
|
||||
QByteArray &buffer,
|
||||
std::vector<Frame> &output);
|
||||
|
||||
} // namespace MTP::WebProxy
|
||||
1593
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_transport.cpp
Normal file
1593
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_transport.cpp
Normal file
File diff suppressed because it is too large
Load Diff
73
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_transport.h
Normal file
73
Telegram/SourceFiles/mtproto/web_proxy/web_proxy_transport.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "mtproto/mtproto_proxy_data.h"
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QPointer>
|
||||
#include <rpl/producer.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace MTP::WebProxy {
|
||||
|
||||
class Transport final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class State {
|
||||
Idle,
|
||||
WaitingForBrowser,
|
||||
Connecting,
|
||||
Connected,
|
||||
Failed,
|
||||
};
|
||||
struct StateChange {
|
||||
ProxyData proxy;
|
||||
State state = State::Idle;
|
||||
QString browser;
|
||||
};
|
||||
struct StreamHandlers {
|
||||
QPointer<QObject> context;
|
||||
Fn<void()> connected;
|
||||
Fn<void(QByteArray)> data;
|
||||
Fn<void()> disconnected;
|
||||
Fn<void()> failed;
|
||||
};
|
||||
|
||||
static void Activate(const ProxyData &proxy, bool openBrowser = true);
|
||||
static void Deactivate();
|
||||
static void Shutdown();
|
||||
[[nodiscard]] static Transport *Instance();
|
||||
[[nodiscard]] static State CurrentState(const ProxyData &proxy);
|
||||
[[nodiscard]] static rpl::producer<StateChange> StateChanges();
|
||||
static void OpenBrowser(const ProxyData &proxy);
|
||||
[[nodiscard]] static uint32 NextStreamId();
|
||||
|
||||
void registerStream(uint32 streamId, StreamHandlers handlers);
|
||||
void closeStream(uint32 streamId);
|
||||
void sendData(uint32 streamId, QByteArray data);
|
||||
void grantWindow(uint32 streamId, uint32 amount);
|
||||
|
||||
~Transport();
|
||||
|
||||
private:
|
||||
Transport();
|
||||
[[nodiscard]] bool reservePending(int bytes);
|
||||
void releasePending(int bytes, int items);
|
||||
|
||||
class Private;
|
||||
const std::unique_ptr<Private> _private;
|
||||
std::atomic<int64> _pendingBytes = 0;
|
||||
std::atomic<int> _pendingItems = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace MTP::WebProxy
|
||||
419
docs/web-proxy-plan.md
Normal file
419
docs/web-proxy-plan.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# Telegram Desktop WEB proxy: refined client design
|
||||
|
||||
The hosted half is specified in `../tproxy-server/PLAN.md`. Its multiplexing frame
|
||||
format and `MessageChannel` contract are authoritative. This document records the
|
||||
reviewed Telegram Desktop design and the implementation now present in this tree.
|
||||
The server-dependent execution procedure is intentionally separate in
|
||||
`docs/web-proxy-test-plan.md`.
|
||||
|
||||
## 1. Scope and invariant
|
||||
|
||||
WEB is an MTProxy whose carrier is the user's real browser:
|
||||
|
||||
```text
|
||||
MTProto session threads
|
||||
-> TcpConnection (existing MTProxy obfuscation and AES-CTR)
|
||||
-> WebProxySocket (one logical stream)
|
||||
-> process-wide WebProxy::Transport (one worker thread)
|
||||
-> authenticated ws://127.0.0.1:<random>/transport
|
||||
-> loopback parent page
|
||||
-> MessageChannel
|
||||
-> https://relay.example/?bridge=<derived-capability> iframe
|
||||
-> HTTPS carrier
|
||||
-> hosted relay
|
||||
-> stock MTProxy
|
||||
-> Telegram
|
||||
```
|
||||
|
||||
The central invariant is that Telegram Desktop opens no external MTProto socket
|
||||
while WEB is active. The only external connection in this path is made by the real
|
||||
browser. The browser and hosted relay see only bytes already transformed by the
|
||||
existing MTProxy protocol layer; the MTProxy secret is never placed in HTML or
|
||||
JavaScript.
|
||||
|
||||
## 2. Design decisions
|
||||
|
||||
The initial draft left several architectural choices open. They are now fixed:
|
||||
|
||||
1. There is one transport per process, not per account. Proxy selection is already
|
||||
process-wide, so all accounts using the selected WEB proxy share one browser tab
|
||||
and one multiplexed browser carrier.
|
||||
2. The transport owns a dedicated `QThread`. `QTcpServer`, accepted loopback
|
||||
sockets, WebSocket framing, mux state, queues, and windows live only there.
|
||||
3. `WebProxySocket` and the transport are compiled in the main `Telegram` target.
|
||||
`connection_tcp.cpp`, the only factory site retaining the full `ProxyData`, is
|
||||
already in that target. No reverse dependency from `td_mtproto` is introduced.
|
||||
4. The serialized `host` field stores only the canonical lowercase ASCII/IDNA
|
||||
A-label hostname. Scheme, port, path, query, fragment, user info, IP addresses,
|
||||
and single-label names are rejected. `port` is fixed to `443`; `password` stores
|
||||
the MTProxy secret.
|
||||
5. WEB is manual-entry-only in v1. It has no `tg://proxy` share/import format.
|
||||
6. Inactive WEB entries are not checked and are removed from proxy rotation's
|
||||
candidate order in v1. Checking would require activating a browser sidecar and
|
||||
must never open tabs for every saved proxy.
|
||||
7. The loopback parent is one inline, dependency-free HTML response. A qrc asset
|
||||
adds no value for this small page and would create another generated-resource
|
||||
dependency.
|
||||
8. RTC keepalive is not part of v1. Browser/tab loss is an ordinary transport loss;
|
||||
Telegram's existing reconnect machinery handles it.
|
||||
|
||||
## 3. Data model and persistence
|
||||
|
||||
`MTP::ProxyData::Type::Web` is appended to the enum and serialized as type code `4`.
|
||||
The existing five-field proxy blob remains unchanged:
|
||||
|
||||
```text
|
||||
type | host | port | user | password
|
||||
```
|
||||
|
||||
WEB maps those fields as follows:
|
||||
|
||||
| Field | WEB meaning |
|
||||
|---|---|
|
||||
| `host` | canonical lowercase ASCII/IDNA A-label hostname |
|
||||
| `port` | fixed value `443` |
|
||||
| `user` | empty |
|
||||
| `password` | existing MTProxy secret syntax |
|
||||
|
||||
Validation requires both a valid DNS hostname and a supported MTProxy secret. Plain
|
||||
16-byte and `dd` random-padding secrets are accepted; `ee` TLS-emulation secrets are
|
||||
rejected because the stock MTProxy would expect an inner TLS-emulation record that
|
||||
this raw relay deliberately does not add. Unknown future serialized type codes
|
||||
deserialize to `None` instead of reaching
|
||||
`Unexpected`, so downgrades skip an unsupported proxy rather than crashing.
|
||||
|
||||
WEB behaves like MTProxy throughout the existing model:
|
||||
|
||||
- `secretFromMtprotoPassword()` accepts WEB.
|
||||
- Qt's application proxy is `NoProxy`; WEB does not affect update or generic HTTP
|
||||
traffic.
|
||||
- custom DC/proxy DNS resolution is disabled because the browser resolves the relay
|
||||
hostname.
|
||||
- calls remain unsupported.
|
||||
- TCP MTProto is enabled and the plain MTProto HTTP connection is disabled.
|
||||
- DC endpoints are ignored; the hosted relay chooses its fixed stock-MTProxy target.
|
||||
- `initConnection` reports the relay hostname and port 443 as client proxy metadata.
|
||||
|
||||
## 4. `WebProxySocket`
|
||||
|
||||
`mtproto/details/mtproto_web_proxy_socket.*` implements `AbstractSocket` as a
|
||||
logical byte stream over the shared transport.
|
||||
|
||||
On `connectToHost`, it registers a new 24-bit stream id. The address and port
|
||||
arguments are intentionally ignored. It emits `connected` after the browser carrier
|
||||
has completed the relay `WELCOME` handshake and the transport has sent `OPEN` for
|
||||
that stream.
|
||||
|
||||
Writes concatenate the one-time MTProxy connection prefix and body before queuing a
|
||||
`DATA` frame. Incoming `DATA` is buffered and exposed through partial `read()` calls.
|
||||
Every successful read replenishes exactly that many bytes of receive credit with a
|
||||
`WINDOW` frame. Transport loss emits `disconnected`; protocol violations, queue
|
||||
overflow, and explicit transport failures emit `error`.
|
||||
|
||||
The existing `TcpConnection` continues to own all MTProxy protocol work. For WEB it
|
||||
uses `secretFromMtprotoPassword()` and `Protocol::Create(secret)` exactly as for
|
||||
MTProxy, then selects `WebProxySocket` at the one socket-factory call site.
|
||||
|
||||
## 5. Process-wide transport and threading
|
||||
|
||||
`mtproto/web_proxy/web_proxy_transport.*` provides a main-thread lifecycle facade and
|
||||
runs all I/O state on its worker thread.
|
||||
|
||||
Main-thread lifecycle:
|
||||
|
||||
- `Activate(proxy)` creates the worker on first use, synchronously installs the
|
||||
selected valid proxy, binds the loopback listener, and auto-opens one browser tab
|
||||
when the selected WEB proxy changes.
|
||||
- `OpenBrowser(proxy)` mints a fresh one-shot capability and opens a new tab on
|
||||
explicit user request.
|
||||
- `Deactivate()` closes streams, accepted clients, and the listener when the app
|
||||
changes away from WEB.
|
||||
- `Shutdown()` runs after MTP accounts have stopped and joins the worker thread.
|
||||
|
||||
Session-thread interaction uses queued calls into the worker. Each stream stores its
|
||||
socket context, and worker-to-socket delivery is queued to that socket's owning
|
||||
thread. `WebProxySocket` destruction unregisters synchronously on the worker before
|
||||
the QObject base destructor can invalidate the context. This creates a strict
|
||||
ordering boundary: notifications already posted remain owned by Qt and are removed
|
||||
with the QObject, while the worker cannot inspect or post through the context after
|
||||
unregistration returns. The global transport pointer is atomic and remains alive
|
||||
until all MTP sessions have been destroyed.
|
||||
|
||||
The state surfaced to settings is:
|
||||
|
||||
```text
|
||||
Idle
|
||||
-> WaitingForBrowser
|
||||
-> Connecting
|
||||
-> Connected
|
||||
-> WaitingForBrowser (tab/local WS lost)
|
||||
-> Failed (protocol/relay failure)
|
||||
```
|
||||
|
||||
## 6. Shared relay frames
|
||||
|
||||
All integers are big-endian. The implementation mirrors server plan section 7:
|
||||
|
||||
```text
|
||||
type:u8 | stream_id:u24 | length:u32 | payload:length
|
||||
```
|
||||
|
||||
Each browser carrier message must contain one or more complete frames. The parser
|
||||
accepts concatenated frames and rejects an empty message or trailing partial frame.
|
||||
A payload is capped at 1 MiB. Known types are:
|
||||
|
||||
| Value | Name | Stream | Client behavior |
|
||||
|---:|---|---:|---|
|
||||
| `0x01` | `OPEN` | >0 | sent once after `WELCOME` |
|
||||
| `0x02` | `DATA` | >0 | opaque MTProxy bytes |
|
||||
| `0x03` | `CLOSE` | >0 | empty payload; closes one logical socket |
|
||||
| `0x04` | `WINDOW` | >0 | four-byte credit delta |
|
||||
| `0x05` | `PING` | 0 | relay-to-client keepalive; answered with `PONG` |
|
||||
| `0x06` | `PONG` | 0 | sent only as the exact `PING` response |
|
||||
| `0x10` | `HELLO` | 0 | client sends payload `01` for protocol v1 |
|
||||
| `0x11` | `WELCOME` | 0 | empty payload; must be the first relay frame |
|
||||
| `0x12` | `AUTH_CHAL` | 0 | reserved for relay-auth v2, rejected in v1 |
|
||||
| `0x13` | `AUTH_RESP` | 0 | reserved for relay-auth v2 |
|
||||
| `0x1f` | `BYE` | 0 | fails current logical streams and closes the carrier |
|
||||
|
||||
An incoming `OPEN`, a stream frame on stream zero, a session frame on a nonzero
|
||||
stream, malformed `WINDOW`, data beyond granted credit, an unknown live stream, or
|
||||
an unknown type is a protocol error for v1. The client retains up to 4096 recently
|
||||
closed stream ids. Well-formed `DATA`, `WINDOW`, and `CLOSE` already in flight for a
|
||||
retained id are discarded; this prevents an ordinary cross-direction close race from
|
||||
failing unrelated multiplexed streams.
|
||||
|
||||
## 7. Flow control and memory bounds
|
||||
|
||||
Both directions start with an implicit 4 MiB per-stream window.
|
||||
|
||||
Downlink flow control is exact: relay `DATA` consumes client receive credit, and
|
||||
Telegram Desktop grants it back only when `WebProxySocket::read()` drains bytes into
|
||||
the MTProto engine. This naturally bounds each socket's unread data.
|
||||
|
||||
Uplink has a constraint the initial draft missed: `AbstractSocket::write()` returns
|
||||
`void` and provides no writable/backpressure event, so it cannot stop the MTProto
|
||||
caller and resume later. The client therefore:
|
||||
|
||||
- spends relay-granted send credit before emitting each `DATA` frame;
|
||||
- splits outgoing data into at most 64 KiB frames;
|
||||
- queues excess data per stream;
|
||||
- coalesces adjacent writes up to 64 KiB and avoids front-removal copies;
|
||||
- fails the stream if its pending uplink exceeds 8 MiB or 1024 queued items;
|
||||
- caps all queued cross-thread uplink data at 64 MiB and 8192 items;
|
||||
- pauses stream flushing when the process-wide loopback socket write queue reaches
|
||||
4 MiB and resumes it as bytes drain;
|
||||
- reserves 64 KiB of that socket budget for control traffic and bounds a separate
|
||||
64 KiB / 1024-frame control queue; and
|
||||
- schedules ready streams round-robin, with at most 256 frames per worker turn.
|
||||
|
||||
If the browser socket makes no write progress for 30 seconds, the carrier fails and
|
||||
normal MTProto reconnect logic replaces it. Exhausting a stream or transport budget
|
||||
also fails promptly rather than allowing unbounded queued worker events. If
|
||||
measurements show sustained multi-megabyte uploads can exhaust these bounds, a
|
||||
future change must add writable backpressure to the `AbstractSocket` contract rather
|
||||
than silently growing memory.
|
||||
|
||||
### 7.1 Performance envelope and built-in HTTP comparison
|
||||
|
||||
The hosted bridge batches up to 2 MiB and runs uplink and downlink concurrently.
|
||||
Each direction is sequenced stop-and-wait in v1, giving an RTT-only busy-direction
|
||||
bound of 40, 20, 10, and 4 MiB/s at 50, 100, 200, and 500 ms browser-to-relay RTT,
|
||||
respectively. Actual results include transfer time, the relay-to-MTProxy leg, and
|
||||
browser scheduling. The 4 MiB stream window is two carrier batches so returned
|
||||
credit does not reproduce the former 256 KiB bottleneck.
|
||||
|
||||
The built-in MTProto HTTP transport also copies request/response bodies and uses an
|
||||
HTTP wait request, but `QNetworkAccessManager` may keep several POSTs active. WEB is
|
||||
therefore more RTT-sensitive today. That serialization, fixed batch size, and most
|
||||
buffer copies are implementation choices; a bounded ordered pipeline or compatible
|
||||
streaming carrier can narrow them. Inherent WEB cost remains one browser process,
|
||||
an extra relay/TLS path, MessageChannel/loopback crossings, and shared-carrier
|
||||
head-of-line exposure. With a well-placed relay, ordinary messaging and moderate
|
||||
media should be in the same practical class as the built-in HTTP transport, while
|
||||
direct TCP/MTProxy remains the latency and peak-throughput reference.
|
||||
|
||||
## 8. Loopback HTTP and WebSocket boundary
|
||||
|
||||
The worker binds `QHostAddress::LocalHost` on an ephemeral port and advertises the
|
||||
numeric origin `http://127.0.0.1:<port>`.
|
||||
|
||||
`GET /` serves the inline parent with `no-store`, `nosniff`, `no-referrer`, and a
|
||||
fresh per-response script nonce. Its strict CSP permits only that nonce-bound
|
||||
bootstrap, the configured HTTPS iframe origin, and its exact local WebSocket
|
||||
endpoint.
|
||||
|
||||
`GET /transport` upgrades to RFC 6455 only when all of the following hold:
|
||||
|
||||
- peer address is loopback;
|
||||
- method/path are exactly `GET /` or the `/transport` upgrade;
|
||||
- `Host` is the exact numeric loopback host and current port;
|
||||
- `Origin` is the exact loopback page origin;
|
||||
- `Upgrade`, `Connection`, version 13, and a valid 16-byte key are present;
|
||||
- duplicate HTTP header names are rejected;
|
||||
- request bodies and transfer encodings are rejected on the local GET boundary;
|
||||
- the HTTP header block is at most 16 KiB.
|
||||
|
||||
Client WebSocket frames must be masked. The parser supports 7/16/64-bit lengths,
|
||||
text, binary, continuation, ping, pong, and close, with a 2 MiB message cap. Server
|
||||
frames are unmasked as required by RFC 6455.
|
||||
|
||||
An accepted local client must complete capability authentication within ten seconds.
|
||||
This bounds silent HTTP connections and unauthenticated WebSockets so they cannot
|
||||
hold all 32 local client slots indefinitely.
|
||||
|
||||
The first complete WebSocket message must be UTF-8 JSON:
|
||||
|
||||
```json
|
||||
{"t":"auth","token":"<capability>","browser":"<user agent summary>"}
|
||||
```
|
||||
|
||||
The capability is 256 random bits, URL-safe base64, carried only in the fragment of
|
||||
the browser URL. The page removes it from the visible URL immediately. It is
|
||||
one-shot, expires after five minutes, and is replaced when another tab is opened. A
|
||||
newly authenticated tab replaces the previous authenticated tab and causes MTProto
|
||||
streams to reconnect rather than attempting unsupported cross-tab resume.
|
||||
|
||||
After authentication:
|
||||
|
||||
- binary WebSocket messages carry one or more shared relay frames;
|
||||
- text messages may only report bridge state as
|
||||
`{"t":"status","state":"connecting|connected|reconnecting|failed"}`.
|
||||
|
||||
If an authenticated browser does not return the required `WELCOME` within 30
|
||||
seconds, the client fails that carrier and closes its local WebSocket. This turns a
|
||||
wrong bridge capability, iframe load failure, or ordinary public response into a
|
||||
recoverable `unavailable` state instead of leaving the settings row connecting
|
||||
forever.
|
||||
|
||||
## 9. Parent page and hosted iframe contract
|
||||
|
||||
The local parent reads and scrubs its independent one-shot loopback capability,
|
||||
connects the local WebSocket, derives the bridge URL, creates an iframe with limited
|
||||
`sandbox` flags, and establishes a `MessageChannel`.
|
||||
|
||||
For a canonical hostname `H` and decoded WEB secret bytes `S`, including the leading
|
||||
`dd` byte when present, it computes:
|
||||
|
||||
```text
|
||||
context = UTF-8("tdesktop-web-proxy-bridge-v1\n" + H)
|
||||
bridge = base64url-no-padding(HMAC-SHA256(key=S, message=context))
|
||||
bridgeUrl = "https://" + H + "/?bridge=" + bridge
|
||||
```
|
||||
|
||||
Normative vectors:
|
||||
|
||||
| Hostname | Decoded secret hex | `bridge` |
|
||||
|---|---|---|
|
||||
| `proxy.example.com` | `000102030405060708090a0b0c0d0e0f` | `MHLEY5PmW1GWqJkSrlmJpvJUiLhBH_QKy6yKg8a0JPk` |
|
||||
| `proxy.example.com` | `dd000102030405060708090a0b0c0d0e0f` | `IpJrt3e7sKtzPyoXy6w-Zj6GGEvsvclN66JzQEfPYLA` |
|
||||
|
||||
The derived capability is constructed in memory and is neither stored nor shown in
|
||||
proxy settings. On iframe load the parent sends exactly:
|
||||
|
||||
```javascript
|
||||
iframe.contentWindow.postMessage(
|
||||
{ t: 'tproxy-init', v: 1 },
|
||||
relayOrigin,
|
||||
[channel.port2]);
|
||||
```
|
||||
|
||||
The target origin is exact and never `*`. Binary messages are transferred as
|
||||
`ArrayBuffer`s in both directions. Frames received locally before iframe
|
||||
initialization are queued briefly and transferred after initialization. The parent
|
||||
does not parse shared relay frames and never receives the MTProxy secret. Both the
|
||||
hosted uplink queue and the parent's local-WebSocket queue are capped at 32 MiB; the
|
||||
hosted queue also caps retained buffer objects at 16384. Exceeding either bound
|
||||
closes the carrier instead of growing browser memory without limit.
|
||||
|
||||
The iframe's status objects update the visible tab and are forwarded to tdesktop.
|
||||
When the local WebSocket closes, the parent sends `{t:'close'}` so the bridge can
|
||||
delete its relay session. Closing the tab drops the local WebSocket, disconnects all
|
||||
logical sockets, and leaves the settings row in `waiting for browser…`. Telegram
|
||||
Desktop does not reopen a tab automatically after a user closes it. Reloading cannot
|
||||
reuse the scrubbed, one-shot loopback capability either. In both cases the row menu
|
||||
provides `Open browser`, which mints a fresh loopback capability and opens a new tab.
|
||||
|
||||
## 10. Settings and app integration
|
||||
|
||||
Proxy settings expose a fourth `WEB` radio option. The editor shows:
|
||||
|
||||
- one proxy hostname field;
|
||||
- one MTProxy secret field;
|
||||
- no socket host/port pair and no username/password controls.
|
||||
|
||||
Rows display only the hostname. The selected row
|
||||
shows the transport lifecycle, and its menu has `Open browser`. WEB remains
|
||||
non-shareable and unsupported for calls. Because the backend is still MTProxy, WEB
|
||||
keeps the existing sponsored-proxy disclosure and promotion refresh behavior.
|
||||
|
||||
Application proxy changes configure/deconfigure the browser transport before MTP
|
||||
sessions restart. WEB follows the MTProxy path in `Session`, `SessionPrivate`, and
|
||||
`TcpConnection`; the global Qt proxy remains disabled for it. Proxy rotation and the
|
||||
settings availability checker deliberately treat inactive WEB entries as unavailable
|
||||
instead of opening a browser.
|
||||
|
||||
## 11. Constraints and boundaries
|
||||
|
||||
- The listener is IPv4 loopback-only and validates peer, host, and origin.
|
||||
- Local authentication requires the minted fragment capability.
|
||||
- The local protocol has no arbitrary destination command. `OPEN` originates only
|
||||
from tdesktop and the relay is expected to dial one configured stock MTProxy.
|
||||
- The configured value is a canonical DNS hostname; HTTPS and port 443 are fixed.
|
||||
- The bridge URL contains only the domain-separated derived capability, never the raw
|
||||
MTProxy secret.
|
||||
- Frame, WebSocket, HTTP-header, local-client-count, receive-window, and
|
||||
pending-uplink bounds prevent unbounded buffering.
|
||||
- The parent iframe uses only `sandbox="allow-scripts allow-same-origin"`.
|
||||
- Payloads and secrets are never logged by this client code.
|
||||
- WEB socket failures do not invoke tdesktop's direct HTTP time-sync fallback.
|
||||
- Relay authentication (`AUTH_CHAL` / `AUTH_RESP`) is not implemented in v1. Adding
|
||||
it requires a fully specified challenge context and server test vectors; it must be
|
||||
computed in tdesktop without passing the secret to JavaScript.
|
||||
|
||||
## 12. Hosted-server requirements before execution testing
|
||||
|
||||
The server must provide all of these before the separate test plan can pass:
|
||||
|
||||
1. `https://<hostname>/?bridge=<derived-capability>` implements the exact derivation,
|
||||
ordinary-site fallback, `MessageChannel`, close, and status contracts above.
|
||||
2. Its CSP allows framing by random numeric loopback origins. A suitable source is
|
||||
`http://127.0.0.1:*`; `X-Frame-Options` must not block the embed.
|
||||
3. The bridge accepts the v1 `HELLO` frame, establishes a reliable ordered carrier,
|
||||
and returns `WELCOME` before stream traffic.
|
||||
4. The relay implements all v1 stream frames, the implicit 4 MiB windows, and
|
||||
deduplicated/cursor-based reliability for polling carriers.
|
||||
5. Every `OPEN` dials only the configured stock MTProxy endpoint.
|
||||
6. The hosted code never logs frame payloads.
|
||||
7. The v1 HTTPS long-poll carrier is operational; the deployed bridge does not
|
||||
require a public WebSocket or another carrier.
|
||||
|
||||
## 13. Implementation inventory
|
||||
|
||||
Core transport:
|
||||
|
||||
- `Telegram/SourceFiles/mtproto/web_proxy/web_proxy_frame.{h,cpp}`
|
||||
- `Telegram/SourceFiles/mtproto/web_proxy/web_proxy_transport.{h,cpp}`
|
||||
- `Telegram/SourceFiles/mtproto/details/mtproto_web_proxy_socket.{h,cpp}`
|
||||
|
||||
Integration:
|
||||
|
||||
- `mtproto_proxy_data.*`, `core_settings_proxy.cpp`
|
||||
- `connection_tcp.cpp`, `session.cpp`, `session_private.cpp`, `proxy_check.cpp`
|
||||
- `application.cpp`, `main_account.cpp`
|
||||
- `boxes/connection_box.{h,cpp}`, `lang.strings`
|
||||
- `Telegram/CMakeLists.txt`
|
||||
|
||||
The client-side implementation is complete without the hosted server. The arm64
|
||||
Debug build passes; remaining verification is the hosted protocol/loopback and
|
||||
browser matrix in `docs/web-proxy-test-plan.md`, followed by other platform builds.
|
||||
|
||||
## 14. Explicitly deferred
|
||||
|
||||
- public deep-link/share format;
|
||||
- checking inactive WEB proxies and auto-rotation into them;
|
||||
- cross-tab or cross-process relay-session resume;
|
||||
- relay-auth v2;
|
||||
- RTC/background-tab keepalive tricks;
|
||||
- alternate bridge paths, ports, or non-HTTPS relay origins;
|
||||
- expanding `AbstractSocket` with true uplink writable backpressure.
|
||||
407
docs/web-proxy-test-plan.md
Normal file
407
docs/web-proxy-test-plan.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# Telegram Desktop WEB proxy: server-ready execution test plan
|
||||
|
||||
Execute this document only after a hosted relay matching
|
||||
`../tproxy-server/PLAN.md` is deployed. Record every requested value and artifact in
|
||||
one run log. Do not put the proxy secret, Telegram auth keys, message contents, or
|
||||
the loopback fragment capability in logs or screenshots.
|
||||
|
||||
## 1. Release gate
|
||||
|
||||
WEB is ready for wider testing only when all P0 and P1 cases below pass on at least
|
||||
one Chromium browser and the supported browser/platform matrix has no unexplained
|
||||
P0 failure.
|
||||
|
||||
Severity:
|
||||
|
||||
- P0: transport boundary, corruption, crash, login, basic send/receive, or total
|
||||
reconnect failure.
|
||||
- P1: large transfer, concurrency, bounded memory, lifecycle recovery, or carrier
|
||||
fallback failure.
|
||||
- P2: status text, browser-specific lifecycle annoyance, or non-blocking polish.
|
||||
|
||||
## 2. Inputs to obtain from the server operator
|
||||
|
||||
Record these before starting:
|
||||
|
||||
| Input | Value |
|
||||
|---|---|
|
||||
| Relay hostname | `________________` |
|
||||
| Bridge deployment/version | `________________` |
|
||||
| Relay binary commit/version | `________________` |
|
||||
| Stock MTProxy version | `________________` |
|
||||
| MTProxy secret | store outside this document |
|
||||
| Long-poll enabled | yes / no |
|
||||
| Server log location | `________________` |
|
||||
| Server metrics endpoint/dashboard | `________________` |
|
||||
| Test start/end in UTC | `________________` |
|
||||
| Tester network and country | `________________` |
|
||||
|
||||
The operator must confirm:
|
||||
|
||||
- the bridge root response can be framed by `http://127.0.0.1:*` and is not prevented
|
||||
by `X-Frame-Options`;
|
||||
- `HELLO` payload `01`, `WELCOME`, implicit 4 MiB windows, and all v1 frame types
|
||||
match `docs/web-proxy-plan.md`;
|
||||
- the polling carrier provides ordered retry/deduplication;
|
||||
- `OPEN` can dial only the configured local stock MTProxy;
|
||||
- payload logging is disabled.
|
||||
|
||||
Stop if any item is false. A client run cannot produce a meaningful result against
|
||||
an incompatible bridge.
|
||||
|
||||
## 3. Client build and isolation
|
||||
|
||||
1. Build the exact candidate commit in Debug:
|
||||
|
||||
```bash
|
||||
cmake --build out --config Debug --target Telegram
|
||||
```
|
||||
|
||||
2. Record the commit, build timestamp, OS version, browser name/version, and whether
|
||||
the browser is managed by enterprise policy.
|
||||
3. Use a disposable Telegram test account and a separate portable/test profile. Do
|
||||
not overwrite an existing personal portable profile.
|
||||
4. Preserve Debug logs for the run, but verify they contain no proxy secret or frame
|
||||
payload.
|
||||
5. Start with all browser developer tools closed; opening DevTools changes background
|
||||
throttling and would invalidate lifecycle observations.
|
||||
6. Disable unrelated VPNs/proxies for baseline. Record DNS-over-HTTPS, browser proxy,
|
||||
and system proxy state.
|
||||
|
||||
## 4. Hosted endpoint preflight
|
||||
|
||||
Run from the client machine:
|
||||
|
||||
```bash
|
||||
curl -fsS -D /tmp/tproxy-site.headers -o /tmp/tproxy-site.body \
|
||||
https://RELAY_HOSTNAME/
|
||||
curl -fsS -D /tmp/tproxy-invalid.headers -o /tmp/tproxy-invalid.body \
|
||||
'https://RELAY_HOSTNAME/?bridge=invalid'
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
- TLS certificate and hostname are valid with no warning or redirect to HTTP;
|
||||
- `/` looks like the intended ordinary site and contains no transport error details;
|
||||
- an invalid or missing `bridge` query returns the same ordinary site without
|
||||
transport-branded errors;
|
||||
- server unit tests pass both capability vectors from `docs/web-proxy-plan.md`;
|
||||
- the valid bridge response has a compatible `frame-ancestors` CSP and no
|
||||
incompatible `X-Frame-Options`, verified without recording its query;
|
||||
- transport APIs reject unsupported methods and malformed session identifiers;
|
||||
- server health shows the stock MTProxy backend reachable.
|
||||
|
||||
Delete the captured bodies after inspection if the deployment embeds any
|
||||
configuration. Keep sanitized headers with the run artifacts.
|
||||
|
||||
## 5. Configuration and first connection (P0)
|
||||
|
||||
1. Launch the candidate.
|
||||
2. Open Settings -> Advanced -> Connection type -> Proxy settings -> Add proxy.
|
||||
3. Select `WEB`.
|
||||
4. Enter the relay hostname only and the MTProxy secret.
|
||||
5. Confirm that these invalid inputs are rejected without saving:
|
||||
|
||||
- a value containing `http://` or `https://`;
|
||||
- a hostname with an explicit port;
|
||||
- a value with username, path, query, or fragment;
|
||||
- an IPv4 or IPv6 address;
|
||||
- a single-label name such as `localhost`;
|
||||
- an invalid IDNA name, empty label, overlong label, or trailing dot;
|
||||
- invalid or unsupported MTProxy secret;
|
||||
- an `ee` TLS-emulation MTProxy secret;
|
||||
- empty hostname or secret.
|
||||
|
||||
6. Save the valid entry and enable it.
|
||||
7. Confirm exactly one default-browser tab opens at a numeric
|
||||
`http://127.0.0.1:<ephemeral>/#<capability>` URL.
|
||||
8. Confirm the fragment disappears immediately and the page says it is connecting.
|
||||
9. Confirm the settings row moves through `waiting for browser…` / `connecting…` to
|
||||
`online` and the tab says it is connected.
|
||||
10. Confirm Telegram loads dialogs and receives updates.
|
||||
|
||||
Pass conditions:
|
||||
|
||||
- no crash, assertion, TLS warning, CORS/LNA prompt, mixed-content error, or iframe
|
||||
refusal;
|
||||
- server sees one relay session and the expected MTProxy stream connections;
|
||||
- the iframe requests exactly `https://<canonical-host>/?bridge=<43-char-capability>`;
|
||||
- the browser page, DOM, network request headers, and console never contain the
|
||||
MTProxy secret;
|
||||
- closing proxy settings does not affect the connection.
|
||||
|
||||
Repeat once with a profile secret that is valid MTProxy syntax but is not configured
|
||||
on staging. The ordinary-site fallback must match other root responses, and the
|
||||
client must leave `connecting…` for `not available` within 30 seconds rather than
|
||||
waiting forever.
|
||||
|
||||
## 6. Network-origin invariant (P0)
|
||||
|
||||
While WEB is the only enabled Telegram proxy, collect connection ownership with an
|
||||
OS tool. On macOS, for example:
|
||||
|
||||
```bash
|
||||
lsof -nP -iTCP -sTCP:ESTABLISHED | egrep 'Telegram|Chrome|Chromium|Edge|Safari|Firefox'
|
||||
```
|
||||
|
||||
Also capture a short packet trace or firewall connection log if permitted.
|
||||
|
||||
Verify:
|
||||
|
||||
- Telegram Desktop connects to `127.0.0.1:<ephemeral>` only for this transport;
|
||||
- Telegram Desktop has no external connection to the relay origin, stock MTProxy,
|
||||
or Telegram DCs for MTProto traffic;
|
||||
- a WEB connection failure does not trigger tdesktop's HTTP time-sync fallback;
|
||||
- the browser process owns the external HTTPS connection to the relay;
|
||||
- DNS resolution of the relay is attributable to the browser/system resolver, not a
|
||||
custom proxy/DC resolver in tdesktop;
|
||||
- external carrier traffic remains inside the browser-owned TLS connection.
|
||||
|
||||
Account for unrelated Telegram HTTP traffic such as update checks before declaring
|
||||
a failure. The invariant applies to the MTProto transport, not every auxiliary HTTP
|
||||
request made by the application.
|
||||
|
||||
## 7. Functional traffic (P0/P1)
|
||||
|
||||
Run in order, checking both client behavior and relay/MTProxy stream metrics:
|
||||
|
||||
1. Fresh-account login, including code entry and 2FA if available. P0.
|
||||
2. Dialog/history load and live incoming updates. P0.
|
||||
3. Send and receive plain messages in private and group chats. P0.
|
||||
4. Send and receive stickers, reactions, edits, deletes, and read receipts. P1.
|
||||
5. Download thumbnails and several small media files. P0.
|
||||
6. Download one file larger than 1 GiB. P1.
|
||||
7. Upload one file large enough to exceed the 4 MiB window many times. P1.
|
||||
8. Stream a video while downloading another file. P1.
|
||||
9. Open media from CDN-backed storage and confirm shifted/CDN DC streams work. P1.
|
||||
10. Leave the client idle for 30 minutes, then send and receive immediately. P1.
|
||||
|
||||
For large transfers record:
|
||||
|
||||
- bytes and final content hash;
|
||||
- average and minimum throughput;
|
||||
- tdesktop, browser, and relay peak memory;
|
||||
- number of logical streams and stock-MTProxy sockets;
|
||||
- reconnect/retry count;
|
||||
- pending-uplink overflow or flow-control errors.
|
||||
|
||||
Run the same payload once through direct MTProxy and once through tdesktop's built-in
|
||||
HTTP transport under the same controlled capacity/RTT when those controls are
|
||||
reachable. On a controlled link with at least 100 Mbit/s capacity, WEB should
|
||||
sustain at least 40 Mbit/s at 200 ms browser-to-relay RTT and 20 Mbit/s at 500 ms,
|
||||
in both a foreground and an ordinarily hidden tab on each supported browser.
|
||||
Also report WEB/direct-HTTP ratios for message round-trip p50/p95 and bulk transfer;
|
||||
route differences must be reported separately from transport overhead.
|
||||
|
||||
No transfer may corrupt, silently truncate, duplicate an upload, or grow client
|
||||
memory without returning toward baseline after completion.
|
||||
|
||||
## 8. Multiplexing and concurrency (P1)
|
||||
|
||||
1. Start at least 16 simultaneous media downloads across chats.
|
||||
2. Send messages continuously during the downloads.
|
||||
3. Start a large upload at the same time.
|
||||
4. If multi-account is available, sign in to two disposable accounts and generate
|
||||
traffic on both.
|
||||
5. Confirm all logical streams use the same authenticated local WebSocket and browser
|
||||
relay session.
|
||||
6. Confirm one slow or window-exhausted stream does not block unrelated streams.
|
||||
7. Cancel half the transfers and verify the corresponding `CLOSE`s release relay and
|
||||
stock-MTProxy resources.
|
||||
8. Let the remainder finish and compare hashes.
|
||||
|
||||
Monitor thread sanitizer output if a TSan build is practical. Otherwise run for at
|
||||
least two hours while watching for cross-thread QObject warnings, stale stream
|
||||
delivery, stream-id mixups, growing queues, and use-after-free crashes.
|
||||
|
||||
Add a focused destruction race run that repeatedly opens and destroys logical WEB
|
||||
sockets while the worker concurrently delivers `connected`, `DATA`, `CLOSE`, and
|
||||
failure notifications. Run it under ASan and TSan where supported. No notification
|
||||
may begin after synchronous stream unregistration returns, and no callback may run
|
||||
against a socket whose destructor has started.
|
||||
|
||||
## 9. Browser and transport lifecycle (P0/P1)
|
||||
|
||||
Execute each case from a connected baseline:
|
||||
|
||||
| Case | Expected result |
|
||||
|---|---|
|
||||
| Close the sidecar tab | all logical sockets disconnect; row says waiting; no tab auto-reopens |
|
||||
| Row menu -> Open browser | fresh fragment capability; new tab authenticates; Telegram reconnects |
|
||||
| Refresh the tab | consumed capability is not reusable; row says waiting; `Open browser` restores the connection with a fresh capability |
|
||||
| Open browser twice | newest authenticated tab replaces the old one; no stream crosses sessions |
|
||||
| Quit browser | same as tab loss; tdesktop remains responsive |
|
||||
| Restart browser and use Open browser | clean reconnection |
|
||||
| Restart tdesktop with WEB saved/enabled | new loopback port/token and one new tab; stale old tab cannot attach |
|
||||
| Disable WEB | listener closes, browser loses local WS, normal connection policy resumes |
|
||||
| Switch WEB A -> non-WEB -> WEB A | clean teardown and reactivation |
|
||||
| Edit WEB hostname or secret | old transport closes; new settings take effect; no old relay traffic remains |
|
||||
| System sleep 5 minutes | reconnect after wake without corruption or permanent spinner |
|
||||
| Network down/up | browser carrier and MTProto recover within normal retry bounds |
|
||||
|
||||
Record whether the browser freezes/discards the active tab under battery/energy
|
||||
saving. This is evidence for or against a future keepalive feature, not a v1 pass
|
||||
condition if explicit `Open browser` recovers correctly.
|
||||
|
||||
## 10. Carrier reliability and server faults (P1)
|
||||
|
||||
Coordinate these with the server operator:
|
||||
|
||||
1. Drop an empty long-poll request or response. Verify bounded retry and continued
|
||||
Telegram usability.
|
||||
2. Drop one nonempty downlink response after the relay has assigned a cursor. Verify the next
|
||||
request replays it once and tdesktop receives bytes once.
|
||||
3. Drop an uplink response after the relay has processed the sequence. Verify retry
|
||||
deduplication prevents a second write to stock MTProxy.
|
||||
4. Add 1%, then 5%, packet loss and 200-500 ms latency. Verify ordered recovery.
|
||||
5. Restart the web relay while preserving or intentionally discarding session state;
|
||||
record expected bridge status and MTProto reconnection.
|
||||
6. Restart stock MTProxy only. Affected logical streams must close/reconnect without
|
||||
breaking the browser carrier.
|
||||
7. Return malformed frame length, unknown frame type, invalid stream zero usage,
|
||||
zero/invalid `WINDOW`, and data beyond granted credit in a controlled staging
|
||||
environment. The client must close the carrier/streams cleanly, remain responsive,
|
||||
and show no memory error.
|
||||
8. Send `BYE`. Current streams must fail and reconnect according to the bridge/server
|
||||
recovery policy.
|
||||
9. Race client-side close against backend EOF and delayed `DATA`/`WINDOW`/`CLOSE`.
|
||||
The closed stream may discard late frames, but unrelated streams must remain live.
|
||||
10. Stall downlink reads and send highly fragmented one-byte/empty-control patterns.
|
||||
Verify the relay enforces byte and item budgets, returns at most 4096 frames in
|
||||
one body, coalesces adjacent credit, and returns near baseline heap use afterward.
|
||||
11. Issue bridge requests past the per-IP burst/rate and unused-token limits. Other
|
||||
source IPs must retain bounded access, expired tokens must release their slots,
|
||||
and the public fallback must not expose why a request was limited.
|
||||
|
||||
## 11. Loopback validation tests (P0)
|
||||
|
||||
Use a purpose-built local test client; do not paste the real capability into shell
|
||||
history.
|
||||
|
||||
Verify rejection of:
|
||||
|
||||
- a connection to a non-loopback interface (the port must not be listening there);
|
||||
- wrong `Host`;
|
||||
- absent, wrong, or cross-origin `Origin`;
|
||||
- duplicate HTTP header names, including `Host` and `Origin`;
|
||||
- invalid WebSocket key/version/upgrade headers;
|
||||
- an unmasked client frame;
|
||||
- invalid fragmentation or control-frame fragmentation;
|
||||
- a WebSocket message larger than 2 MiB;
|
||||
- an HTTP header block larger than 16 KiB;
|
||||
- a GET with `Content-Length` or `Transfer-Encoding`;
|
||||
- a silent connection or unauthenticated WebSocket held past ten seconds;
|
||||
- first message not being an auth object;
|
||||
- wrong, reused, expired, or empty capability;
|
||||
- binary data before authentication;
|
||||
- non-status text after authentication.
|
||||
|
||||
Then verify:
|
||||
|
||||
- a consumed capability cannot authenticate a second socket;
|
||||
- minting via `Open browser` invalidates any unconsumed earlier capability;
|
||||
- a newly authenticated browser replaces the old one and forces logical reconnect;
|
||||
- `GET /` never includes the capability or MTProxy secret;
|
||||
- the local protocol cannot request an arbitrary host or port;
|
||||
- malformed input causes bounded close/failure, not a crash or growing buffer.
|
||||
- the loopback parent CSP contains a fresh nonce and does not permit arbitrary
|
||||
inline script.
|
||||
|
||||
## 12. Persistence and compatibility (P1)
|
||||
|
||||
1. Save WEB, quit cleanly, relaunch, and verify hostname/secret/type survive with
|
||||
port fixed to 443.
|
||||
2. Switch among System, Disabled, SOCKS5/HTTP/MTProxy, and WEB; verify Qt's global
|
||||
application proxy is never set to the WEB hostname.
|
||||
3. Corrupt a copy of the serialized proxy type to an unknown future value and verify
|
||||
the candidate skips it instead of crashing. Never modify the only real settings
|
||||
file.
|
||||
4. Launch an older binary against a disposable copy of settings containing WEB and
|
||||
document its behavior. The new binary handles unknown types; old binary
|
||||
behavior may still require a release-note warning.
|
||||
5. Confirm WEB cannot be shared/copied as a `tg://proxy` link and inactive WEB rows do
|
||||
not auto-open a browser during availability checks or proxy rotation.
|
||||
6. Confirm a saved and enabled WEB proxy intentionally opens one replacement tab at
|
||||
application startup, while closing that tab does not cause an automatic reopen
|
||||
loop.
|
||||
|
||||
## 13. Browser/platform matrix
|
||||
|
||||
At minimum execute sections 5, 6, 7 (small traffic), 9, and 11 on each supported
|
||||
combination available:
|
||||
|
||||
| OS | Browser | Version | Result |
|
||||
|---|---|---|---|
|
||||
| Windows | Chrome | | |
|
||||
| Windows | Edge | | |
|
||||
| Windows | Firefox | | |
|
||||
| macOS | Chrome | | |
|
||||
| macOS | Safari | | |
|
||||
| macOS | Firefox | | |
|
||||
| all-other desktop | Chrome/Chromium | | |
|
||||
| all-other desktop | Firefox | | |
|
||||
|
||||
Pay special attention to iframe CSP, loopback WebSocket Origin, mixed-content rules,
|
||||
background-tab throttling, default-browser launch, and managed-browser policies.
|
||||
|
||||
## 14. Unreliable-MTProto network field test (P0)
|
||||
|
||||
This is the product hypothesis test and cannot be replaced by a lab run.
|
||||
|
||||
1. Use a network where direct Telegram and ordinary MTProxy are demonstrably
|
||||
unreliable.
|
||||
2. Record those comparison results immediately before WEB testing.
|
||||
3. Confirm the relay's ordinary site is reachable in the chosen browser.
|
||||
4. Enable WEB and repeat login/history/message/media cases.
|
||||
5. Capture sanitized connection ownership and traffic metadata.
|
||||
6. Confirm the observable external client is the browser and all carrier requests are
|
||||
ordinary same-origin HTTPS; no public WebSocket is required.
|
||||
7. Repeat at two times of day and, if possible, through two access providers.
|
||||
|
||||
Pass means WEB works while both direct Telegram and ordinary MTProxy controls fail,
|
||||
without requiring a browser certificate exception or a nonstandard network setting.
|
||||
|
||||
## 15. Deployment-boundary checks (P1)
|
||||
|
||||
1. Run the Go unit suite and race detector, including concurrent carrier, bootstrap,
|
||||
queue-fragmentation, downlink-frame-count, and goroutine-shutdown cases.
|
||||
2. Validate the shipped Caddyfile with the pinned Caddy build and exercise public
|
||||
root, bridge root, API, static asset, and error routes through Caddy.
|
||||
3. Confirm `/debug/pprof/` is 404 on the admin listener by default and appears only
|
||||
when `enable_pprof` is explicitly enabled.
|
||||
4. Confirm the MTProxy source archive matches the pinned commit checksum and its
|
||||
Makefile executes as `mtproxy`, not root.
|
||||
5. From shells running as `caddy` and `tproxy`, verify the MTProxy command line is
|
||||
hidden by the supplied `/proc` restrictions. Record that root remains able to
|
||||
inspect the stock upstream `-S` argument.
|
||||
6. Start sessions with active backend reads and writes, then stop the relay. Shutdown
|
||||
must complete inside its configured deadline with no backend goroutine left.
|
||||
|
||||
## 16. Exit criteria and run report
|
||||
|
||||
Attach or link:
|
||||
|
||||
- client/server/bridge/MTProxy versions;
|
||||
- sanitized server headers and logs;
|
||||
- Debug build result;
|
||||
- functional and lifecycle checklist;
|
||||
- transfer hashes and performance/memory table;
|
||||
- connection-owner evidence;
|
||||
- long-poll retry/replay evidence;
|
||||
- loopback and protocol validation results;
|
||||
- browser/platform matrix;
|
||||
- unreliable-MTProto network comparisons and outcome;
|
||||
- every defect with severity, exact reproduction, expected/actual result, timestamps,
|
||||
and relevant sanitized logs.
|
||||
|
||||
Final decision:
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| All P0 passed | |
|
||||
| All P1 passed or explicitly waived | |
|
||||
| Logs contain no secrets or payloads | |
|
||||
| Memory and queues bounded | |
|
||||
| Long-poll reliability proven | |
|
||||
| Unreliable-MTProto network result confirmed | |
|
||||
| Ready for wider testing | yes / no |
|
||||
Reference in New Issue
Block a user