Add proxy auto-rotation option.

This commit is contained in:
John Preston
2026-04-16 14:50:06 +07:00
parent 1b96772126
commit 2cd90eb3ce
14 changed files with 958 additions and 133 deletions
+4
View File
@@ -503,6 +503,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
core/proxy_rotation_manager.cpp
core/proxy_rotation_manager.h
core/cached_webview_availability.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
@@ -1436,6 +1438,8 @@ PRIVATE
mtproto/facade.h
mtproto/mtp_instance.cpp
mtproto/mtp_instance.h
mtproto/proxy_check.cpp
mtproto/proxy_check.h
mtproto/sender.h
mtproto/session.cpp
mtproto/session.h
+4
View File
@@ -1239,6 +1239,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_use_system_settings" = "Use system proxy settings";
"lng_proxy_use_custom" = "Use custom proxy";
"lng_proxy_use_for_calls" = "Use proxy for calls";
"lng_proxy_auto_switch" = "Auto-switch proxies";
"lng_proxy_auto_switch_about" = "You can choose how quickly the app should auto-connect to the nearest active proxy if the current one stops working.";
"lng_proxy_auto_switch_timeout#one" = "{count} s";
"lng_proxy_auto_switch_timeout#other" = "{count} s";
"lng_proxy_about" = "Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.";
"lng_proxy_add" = "Add proxy";
"lng_proxy_share" = "Share";
+143 -120
View File
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_account.h"
#include "main/main_session.h"
#include "mtproto/facade.h"
#include "mtproto/proxy_check.h"
#include "settings/settings_common.h"
#include "storage/localstorage.h"
#include "ui/basic_click_handlers.h"
@@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/password_input.h"
@@ -62,6 +64,20 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
using ProxyData = MTP::ProxyData;
[[nodiscard]] int ClosestProxyRotationTimeoutSection(int value) {
auto result = 0;
auto bestDistance = 0;
for (auto i = 0; i != int(Core::SettingsProxy::kProxyRotationTimeouts.size()); ++i) {
const auto current = Core::SettingsProxy::kProxyRotationTimeouts[i];
const auto distance = (current > value) ? (current - value) : (value - current);
if ((i == 0) || (distance < bestDistance)) {
result = i;
bestDistance = distance;
}
}
return result;
}
[[nodiscard]] std::vector<QString> ExtractUrlsSimple(const QString &input) {
auto urls = std::vector<QString>();
static auto urlRegex = QRegularExpression(R"((https?:\/\/[^\s]+))");
@@ -376,12 +392,16 @@ private:
void setupButtons(int id, not_null<ProxyRow*> button);
int rowHeight() const;
void refreshProxyForCalls();
void refreshProxyRotation();
not_null<ProxiesBoxController*> _controller;
Core::SettingsProxy &_settings;
QPointer<Ui::Checkbox> _tryIPv6;
std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyRotation;
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _proxyRotationOptions;
QPointer<Ui::SettingsSlider> _proxyRotationTimeout;
QPointer<Ui::DividerLabel> _about;
base::unique_qptr<Ui::RpWidget> _noRows;
object_ptr<Ui::VerticalLayout> _initialWrap;
@@ -900,6 +920,47 @@ void ProxiesBox::setupContent() {
0,
st::proxyTryIPv6Padding.right(),
st::proxyTryIPv6Padding.top()));
_proxyRotation = inner->add(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
inner,
object_ptr<Ui::Checkbox>(
inner,
tr::lng_proxy_auto_switch(tr::now),
_settings.proxyRotationEnabled()),
style::margins(
0,
st::proxyUsePadding.top(),
0,
st::proxyUsePadding.bottom())),
style::margins(
st::proxyTryIPv6Padding.left(),
0,
st::proxyTryIPv6Padding.right(),
st::proxyTryIPv6Padding.top()));
_proxyRotationOptions = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
_proxyRotationTimeout = _proxyRotationOptions->entity()->add(
object_ptr<Ui::SettingsSlider>(
_proxyRotationOptions->entity(),
st::settingsSlider),
st::settingsBigScalePadding);
for (const auto seconds : Core::SettingsProxy::kProxyRotationTimeouts) {
_proxyRotationTimeout->addSection(
tr::lng_proxy_auto_switch_timeout(
tr::now,
lt_count,
seconds));
}
_proxyRotationTimeout->setActiveSectionFast(
ClosestProxyRotationTimeoutSection(_settings.proxyRotationTimeout()));
_proxyRotationOptions->entity()->add(
object_ptr<Ui::FlatLabel>(
_proxyRotationOptions->entity(),
tr::lng_proxy_auto_switch_about(tr::now),
st::boxDividerLabel),
st::proxyAboutPadding);
_about = inner->add(
object_ptr<Ui::DividerLabel>(
@@ -922,6 +983,7 @@ void ProxiesBox::setupContent() {
addNewProxy();
}
refreshProxyForCalls();
refreshProxyRotation();
});
_tryIPv6->checkedChanges(
) | rpl::on_next([=](bool checked) {
@@ -931,18 +993,33 @@ void ProxiesBox::setupContent() {
_controller->proxySettingsValue(
) | rpl::on_next([=](ProxyData::Settings value) {
_proxySettings->setValue(value);
refreshProxyForCalls();
refreshProxyRotation();
}, inner->lifetime());
_proxyForCalls->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
_controller->setProxyForCalls(checked);
}, _proxyForCalls->lifetime());
_proxyRotation->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
_controller->setProxyRotationEnabled(checked);
refreshProxyRotation();
}, _proxyRotation->lifetime());
_proxyRotationTimeout->sectionActivated(
) | rpl::on_next([=](int section) {
_controller->setProxyRotationTimeout(
Core::SettingsProxy::kProxyRotationTimeouts[section]);
}, _proxyRotationTimeout->lifetime());
if (_rows.empty()) {
createNoRowsLabel();
}
refreshProxyForCalls();
refreshProxyRotation();
_proxyForCalls->finishAnimating();
_proxyRotation->finishAnimating();
_proxyRotationOptions->finishAnimating();
{
const auto wrap = inner->add(
@@ -987,6 +1064,20 @@ void ProxiesBox::refreshProxyForCalls() {
anim::type::normal);
}
void ProxiesBox::refreshProxyRotation() {
if (!_proxyRotation || !_proxyRotationOptions) {
return;
}
const auto visible = (_proxySettings->current()
== ProxyData::Settings::Enabled)
&& _settings.selected()
&& (_settings.list().size() > 1);
_proxyRotation->toggle(visible, anim::type::normal);
_proxyRotationOptions->toggle(
visible && _proxyRotation->entity()->checked(),
anim::type::normal);
}
int ProxiesBox::rowHeight() const {
return st::proxyRowPadding.top()
+ st::semiboldFont->height
@@ -1029,6 +1120,7 @@ void ProxiesBox::applyView(View &&view) {
} else {
i->second->updateFields(std::move(view));
}
refreshProxyRotation();
}
void ProxiesBox::createNoRowsLabel() {
@@ -1359,95 +1451,7 @@ void ProxyBox::addLabel(
}
using Connection = MTP::details::AbstractConnection;
using Checker = MTP::details::ConnectionPointer;
void ResetProxyCheckers(Checker &v4, Checker &v6) {
v4 = nullptr;
v6 = nullptr;
}
void DropProxyChecker(Checker &v4, Checker &v6, not_null<Connection*> raw) {
if (v4.get() == raw) {
v4 = nullptr;
} else if (v6.get() == raw) {
v6 = nullptr;
}
}
[[nodiscard]] bool HasProxyCheckers(const Checker &v4, const Checker &v6) {
return v4 || v6;
}
void StartProxyCheck(
not_null<MTP::Instance*> mtproto,
const ProxyData &proxy,
Checker &v4,
Checker &v6,
Fn<void(Connection *raw, int ping)> done,
Fn<void(Connection *raw)> fail) {
using Variants = MTP::DcOptions::Variants;
ResetProxyCheckers(v4, v6);
const auto connType = (proxy.type == ProxyData::Type::Http)
? Variants::Http
: Variants::Tcp;
const auto dcId = mtproto->mainDcId();
const auto setup = [&](Checker &checker, const bytes::vector &secret) {
checker = Connection::Create(
mtproto,
connType,
QThread::currentThread(),
secret,
proxy);
const auto raw = checker.get();
raw->connect(raw, &Connection::connected, [=] {
if (done) {
done(raw, raw->pingTime());
}
});
const auto failed = [=] {
if (fail) {
fail(raw);
}
};
raw->connect(raw, &Connection::disconnected, failed);
raw->connect(raw, &Connection::error, failed);
};
if (proxy.type == ProxyData::Type::Mtproto) {
const auto secret = proxy.secretFromMtprotoPassword();
setup(v4, secret);
v4->connectToServer(
proxy.host,
proxy.port,
secret,
dcId,
false);
return;
}
const auto options = mtproto->dcOptions().lookup(
dcId,
MTP::DcType::Regular,
true);
const auto tryConnect = [&](Checker &checker, Variants::Address address) {
const auto &list = options.data[address][connType];
if (list.empty()
|| ((address == Variants::IPv6)
&& !Core::App().settings().proxy().tryIPv6())) {
checker = nullptr;
return;
}
const auto &endpoint = list.front();
setup(checker, endpoint.secret);
checker->connectToServer(
QString::fromStdString(endpoint.ip),
endpoint.port,
endpoint.secret,
dcId,
false);
};
tryConnect(v4, Variants::IPv4);
tryConnect(v6, Variants::IPv6);
}
using Checker = MTP::ProxyCheckConnection;
} // namespace
@@ -1609,18 +1613,19 @@ void ProxiesBoxController::ShowApplyConfirmation(
};
statusLabel->setTextColorOverride(st::proxyRowStatusFg->c);
relayout();
StartProxyCheck(
MTP::StartProxyCheck(
&account->mtp(),
proxy,
Core::App().settings().proxy().tryIPv6(),
state->v4,
state->v6,
[=](Connection *raw, int ping) {
if (!weak || state->finished) {
return;
}
DropProxyChecker(state->v4, state->v6, raw);
MTP::DropProxyChecker(state->v4, state->v6, raw);
state->finished = true;
ResetProxyCheckers(state->v4, state->v6);
MTP::ResetProxyCheckers(state->v4, state->v6);
state->statusValue = TextWithEntities{
tr::lng_proxy_box_table_available(
tr::now,
@@ -1635,13 +1640,13 @@ void ProxiesBoxController::ShowApplyConfirmation(
if (!weak || state->finished) {
return;
}
DropProxyChecker(state->v4, state->v6, raw);
if (!HasProxyCheckers(state->v4, state->v6)) {
MTP::DropProxyChecker(state->v4, state->v6, raw);
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
});
if (!HasProxyCheckers(state->v4, state->v6)) {
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
@@ -1681,9 +1686,9 @@ void ProxiesBoxController::ShowApplyConfirmation(
const auto enableButton = box->addButton(
tr::lng_proxy_box_table_button(),
[=] {
auto &proxies = Core::App().settings().proxy().list();
if (!ranges::contains(proxies, proxy)) {
proxies.push_back(proxy);
auto &settings = Core::App().settings().proxy();
if (settings.indexInList(proxy) < 0) {
settings.addToList(proxy);
}
Core::App().setCurrentProxy(
proxy,
@@ -1719,9 +1724,10 @@ auto ProxiesBoxController::proxySettingsValue() const
void ProxiesBoxController::refreshChecker(Item &item) {
item.state = ItemState::Checking;
const auto id = item.id;
StartProxyCheck(
MTP::StartProxyCheck(
&_account->mtp(),
item.data,
Core::App().settings().proxy().tryIPv6(),
item.checker,
item.checkerv6,
[=](Connection *raw, int pingTime) {
@@ -1732,8 +1738,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
DropProxyChecker(item->checker, item->checkerv6, raw);
ResetProxyCheckers(item->checker, item->checkerv6);
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
MTP::ResetProxyCheckers(item->checker, item->checkerv6);
if (item->state == ItemState::Checking) {
item->state = ItemState::Available;
item->ping = pingTime;
@@ -1748,14 +1754,14 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
DropProxyChecker(item->checker, item->checkerv6, raw);
if (!HasProxyCheckers(item->checker, item->checkerv6)
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
if (!MTP::HasProxyCheckers(item->checker, item->checkerv6)
&& item->state == ItemState::Checking) {
item->state = ItemState::Unavailable;
updateView(*item);
}
});
if (!HasProxyCheckers(item.checker, item.checkerv6)) {
if (!MTP::HasProxyCheckers(item.checker, item.checkerv6)) {
item.state = ItemState::Unavailable;
}
}
@@ -1854,8 +1860,8 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
item->deleted = deleted;
if (deleted) {
auto &proxies = _settings.list();
proxies.erase(ranges::remove(proxies, item->data), end(proxies));
const auto removed = _settings.removeFromList(item->data);
Assert(removed);
if (item->data == _settings.selected()) {
_lastSelectedProxy = _settings.selected();
@@ -1871,16 +1877,19 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
}
}
} else {
auto &proxies = _settings.list();
if (ranges::find(proxies, item->data) == end(proxies)) {
if (_settings.indexInList(item->data) < 0) {
const auto &proxies = _settings.list();
auto insertBefore = item + 1;
while (insertBefore != end(_list) && insertBefore->deleted) {
++insertBefore;
}
auto insertBeforeIt = (insertBefore == end(_list))
? end(proxies)
: ranges::find(proxies, insertBefore->data);
proxies.insert(insertBeforeIt, item->data);
const auto foundIndex = (insertBefore == end(_list))
? int(proxies.size())
: _settings.indexInList(insertBefore->data);
const auto insertIndex = (foundIndex >= 0)
? foundIndex
: int(proxies.size());
_settings.insertToList(insertIndex, item->data);
}
if (!_settings.selected() && _lastSelectedProxy == item->data) {
@@ -1919,8 +1928,8 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {
void ProxiesBoxController::replaceItemWith(
std::vector<Item>::iterator which,
std::vector<Item>::iterator with) {
auto &proxies = _settings.list();
proxies.erase(ranges::remove(proxies, which->data), end(proxies));
const auto removed = _settings.removeFromList(which->data);
Assert(removed);
_views.fire({ which->id });
_list.erase(which);
@@ -1939,10 +1948,8 @@ void ProxiesBoxController::replaceItemValue(
restoreItem(which->id);
}
auto &proxies = _settings.list();
const auto i = ranges::find(proxies, which->data);
Assert(i != end(proxies));
*i = proxy;
const auto replaced = _settings.replaceInList(which->data, proxy);
Assert(replaced);
which->data = proxy;
refreshChecker(*which);
@@ -1978,8 +1985,7 @@ bool ProxiesBoxController::contains(const ProxyData &proxy) const {
}
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
auto &proxies = _settings.list();
proxies.push_back(proxy);
_settings.addToList(proxy);
_list.push_back({ ++_idCounter, proxy });
refreshChecker(_list.back());
@@ -2016,6 +2022,22 @@ void ProxiesBoxController::setProxyForCalls(bool enabled) {
saveDelayed();
}
void ProxiesBoxController::setProxyRotationEnabled(bool enabled) {
if (_settings.proxyRotationEnabled() == enabled) {
return;
}
_settings.setProxyRotationEnabled(enabled);
saveDelayed();
}
void ProxiesBoxController::setProxyRotationTimeout(int value) {
if (_settings.proxyRotationTimeout() == value) {
return;
}
_settings.setProxyRotationTimeout(value);
saveDelayed();
}
void ProxiesBoxController::setTryIPv6(bool enabled) {
if (Core::App().settings().proxy().tryIPv6() == enabled) {
return;
@@ -2027,6 +2049,7 @@ void ProxiesBoxController::setTryIPv6(bool enabled) {
}
void ProxiesBoxController::saveDelayed() {
Core::App().proxyRotationSettingsChanged();
_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
}
@@ -85,6 +85,8 @@ public:
object_ptr<Ui::BoxContent> addNewItemBox();
bool setProxySettings(ProxyData::Settings value);
void setProxyForCalls(bool enabled);
void setProxyRotationEnabled(bool enabled);
void setProxyRotationTimeout(int value);
void setTryIPv6(bool enabled);
rpl::producer<ProxyData::Settings> proxySettingsValue() const;
+15
View File
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/sandbox.h"
#include "core/local_url_handlers.h"
#include "core/launcher.h"
#include "core/proxy_rotation_manager.h"
#include "core/ui_integration.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_emoji_image_loader.h"
@@ -146,6 +147,7 @@ struct Application::Private {
base::Timer quitTimer;
UiIntegration uiIntegration;
Settings settings;
std::unique_ptr<ProxyRotationManager> proxyRotation;
};
Application::Application()
@@ -173,6 +175,7 @@ Application::Application()
, _setupEmailLock(false)
, _autoLockTimer([=] { checkAutoLock(); }) {
Ui::Integration::Set(&_private->uiIntegration);
_private->proxyRotation = std::make_unique<ProxyRotationManager>();
_platformIntegration->init();
@@ -234,6 +237,7 @@ Application::~Application() {
// Domain::finish() and there is a violation on Ensures(started()).
closeAdditionalWindows();
_private->proxyRotation = nullptr;
_domain->finish();
Local::finish();
@@ -833,6 +837,17 @@ void Application::setCurrentProxy(
refreshGlobalProxy();
_proxyChanges.fire({ was, now });
my.connectionTypeChangesNotify();
proxyRotationSettingsChanged();
}
void Application::proxyRotationSettingsChanged() {
_private->proxyRotation->settingsChanged();
}
void Application::checkProxyRotation(
not_null<Main::Account*> account,
int32 state) {
_private->proxyRotation->handleConnectionStateChanged(account, state);
}
auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
+2
View File
@@ -219,6 +219,8 @@ public:
void setCurrentProxy(
const MTP::ProxyData &proxy,
MTP::ProxyData::Settings settings);
void proxyRotationSettingsChanged();
void checkProxyRotation(not_null<Main::Account*> account, int32 state);
[[nodiscard]] rpl::producer<ProxyChange> proxyChanges() const;
void badMtprotoConfigurationError();
@@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/platform/base_platform_info.h"
#include "storage/serialize_common.h"
#include <algorithm>
namespace Core {
namespace {
@@ -88,6 +90,22 @@ namespace {
return result;
}
std::vector<int> NormalizeProxyRotationPreferredIndices(
std::vector<int> indices,
int listSize) {
auto filtered = std::vector<int>();
filtered.reserve(indices.size());
for (const auto index : indices) {
if (index < 0
|| index >= listSize
|| ranges::contains(filtered, index)) {
continue;
}
filtered.push_back(index);
}
return filtered;
}
} // namespace
SettingsProxy::SettingsProxy()
@@ -108,7 +126,7 @@ QByteArray SettingsProxy::serialize() const {
0,
ranges::plus(),
&Serialize::bytearraySize)
+ 1 * sizeof(qint32); // _checkIpWarningShown
+ (4 + int(_proxyRotationPreferredIndices.size())) * sizeof(qint32);
auto stream = Serialize::ByteArrayWriter(size);
stream
<< qint32(_tryIPv6 ? 1 : 0)
@@ -119,7 +137,14 @@ QByteArray SettingsProxy::serialize() const {
for (const auto &i : serializedList) {
stream << i;
}
stream << qint32(_checkIpWarningShown ? 1 : 0);
stream
<< qint32(_checkIpWarningShown ? 1 : 0)
<< qint32(_proxyRotationEnabled ? 1 : 0)
<< qint32(_proxyRotationTimeout)
<< qint32(_proxyRotationPreferredIndices.size());
for (const auto index : _proxyRotationPreferredIndices) {
stream << qint32(index);
}
return std::move(stream).result();
}
@@ -135,6 +160,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
auto settings = ProxySettingsToInt(_settings);
auto listCount = qint32(_list.size());
auto selectedProxy = QByteArray();
auto list = std::vector<MTP::ProxyData>();
if (!stream.atEnd()) {
stream
@@ -144,10 +170,14 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
>> selectedProxy
>> listCount;
if (stream.ok()) {
if (listCount < 0) {
return false;
}
list.reserve(listCount);
for (auto i = 0; i != listCount; ++i) {
QByteArray data;
stream >> data;
_list.push_back(DeserializeProxyData(data));
list.push_back(DeserializeProxyData(data));
}
}
}
@@ -156,6 +186,30 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> checkIpWarningShown;
}
auto proxyRotationEnabled = qint32(_proxyRotationEnabled ? 1 : 0);
if (!stream.atEnd()) {
stream >> proxyRotationEnabled;
}
auto proxyRotationTimeout = qint32(_proxyRotationTimeout);
if (!stream.atEnd()) {
stream >> proxyRotationTimeout;
}
auto preferredCount = qint32(0);
auto preferredIndices = std::vector<int>();
if (!stream.atEnd()) {
stream >> preferredCount;
if (stream.ok()) {
if (preferredCount < 0) {
return false;
}
preferredIndices.reserve(preferredCount);
for (auto i = 0; i != preferredCount; ++i) {
auto index = qint32(0);
stream >> index;
preferredIndices.push_back(index);
}
}
}
if (!stream.ok()) {
LOG(("App Error: "
@@ -166,8 +220,12 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
_tryIPv6 = (tryIPv6 == 1);
_useProxyForCalls = (useProxyForCalls == 1);
_checkIpWarningShown = (checkIpWarningShown == 1);
_proxyRotationEnabled = (proxyRotationEnabled == 1);
setProxyRotationTimeout(proxyRotationTimeout);
_settings = IntToProxySettings(settings);
_selected = DeserializeProxyData(selectedProxy);
_list = std::move(list);
setProxyRotationPreferredIndices(std::move(preferredIndices));
return true;
}
@@ -192,6 +250,32 @@ void SettingsProxy::setCheckIpWarningShown(bool value) {
_checkIpWarningShown = value;
}
const std::vector<int> &SettingsProxy::proxyRotationPreferredIndices() const {
return _proxyRotationPreferredIndices;
}
void SettingsProxy::setProxyRotationPreferredIndices(std::vector<int> value) {
_proxyRotationPreferredIndices = NormalizeProxyRotationPreferredIndices(
std::move(value),
int(_list.size()));
}
bool SettingsProxy::promoteProxyRotationPreferredIndex(int index) {
if (index < 0 || index >= int(_list.size())) {
return false;
}
auto &indices = _proxyRotationPreferredIndices;
const auto i = ranges::find(indices, index);
if (i == begin(indices)) {
return false;
} else if (i != end(indices)) {
std::rotate(begin(indices), i, std::next(i));
} else {
indices.insert(begin(indices), index);
}
return true;
}
bool SettingsProxy::tryIPv6() const {
return _tryIPv6;
}
@@ -208,6 +292,24 @@ void SettingsProxy::setUseProxyForCalls(bool value) {
_useProxyForCalls = value;
}
bool SettingsProxy::proxyRotationEnabled() const {
return _proxyRotationEnabled;
}
void SettingsProxy::setProxyRotationEnabled(bool value) {
_proxyRotationEnabled = value;
}
int SettingsProxy::proxyRotationTimeout() const {
return _proxyRotationTimeout;
}
void SettingsProxy::setProxyRotationTimeout(int value) {
_proxyRotationTimeout = (value > 0)
? value
: kDefaultProxyRotationTimeout;
}
MTP::ProxyData::Settings SettingsProxy::settings() const {
return _settings;
}
@@ -232,6 +334,62 @@ std::vector<MTP::ProxyData> &SettingsProxy::list() {
return _list;
}
void SettingsProxy::setList(std::vector<MTP::ProxyData> value) {
_list = std::move(value);
_proxyRotationPreferredIndices.clear();
}
void SettingsProxy::addToList(MTP::ProxyData value) {
_list.push_back(std::move(value));
}
void SettingsProxy::insertToList(int index, MTP::ProxyData value) {
index = std::clamp(index, 0, int(_list.size()));
for (auto &existing : _proxyRotationPreferredIndices) {
if (existing >= index) {
++existing;
}
}
_list.insert(begin(_list) + index, std::move(value));
}
bool SettingsProxy::removeFromList(const MTP::ProxyData &value) {
const auto i = ranges::find(_list, value);
if (i == end(_list)) {
return false;
}
const auto index = int(i - begin(_list));
_list.erase(i);
for (auto &existing : _proxyRotationPreferredIndices) {
if (existing > index) {
--existing;
}
}
_proxyRotationPreferredIndices.erase(
std::remove(
begin(_proxyRotationPreferredIndices),
end(_proxyRotationPreferredIndices),
index),
end(_proxyRotationPreferredIndices));
return true;
}
bool SettingsProxy::replaceInList(
const MTP::ProxyData &was,
MTP::ProxyData value) {
const auto i = ranges::find(_list, was);
if (i == end(_list)) {
return false;
}
*i = std::move(value);
return true;
}
int SettingsProxy::indexInList(const MTP::ProxyData &value) const {
const auto i = ranges::find(_list, value);
return (i == end(_list)) ? -1 : int(i - begin(_list));
}
rpl::producer<> SettingsProxy::connectionTypeValue() const {
return _connectionTypeChanges.events_starting_with({});
}
@@ -9,10 +9,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_proxy_data.h"
#include <array>
namespace Core {
class SettingsProxy final {
public:
static constexpr auto kProxyRotationTimeouts = std::array{
5,
10,
15,
30,
60,
};
static constexpr auto kDefaultProxyRotationTimeout = 10;
SettingsProxy();
[[nodiscard]] bool isEnabled() const;
@@ -29,6 +40,12 @@ public:
[[nodiscard]] bool useProxyForCalls() const;
void setUseProxyForCalls(bool value);
[[nodiscard]] bool proxyRotationEnabled() const;
void setProxyRotationEnabled(bool value);
[[nodiscard]] int proxyRotationTimeout() const;
void setProxyRotationTimeout(int value);
[[nodiscard]] MTP::ProxyData::Settings settings() const;
void setSettings(MTP::ProxyData::Settings value);
@@ -38,8 +55,20 @@ public:
[[nodiscard]] bool checkIpWarningShown() const;
void setCheckIpWarningShown(bool value);
[[nodiscard]] const std::vector<int> &proxyRotationPreferredIndices() const;
void setProxyRotationPreferredIndices(std::vector<int> value);
[[nodiscard]] bool promoteProxyRotationPreferredIndex(int index);
[[nodiscard]] const std::vector<MTP::ProxyData> &list() const;
[[nodiscard]] std::vector<MTP::ProxyData> &list();
void setList(std::vector<MTP::ProxyData> value);
void addToList(MTP::ProxyData value);
void insertToList(int index, MTP::ProxyData value);
[[nodiscard]] bool removeFromList(const MTP::ProxyData &value);
[[nodiscard]] bool replaceInList(
const MTP::ProxyData &was,
MTP::ProxyData value);
[[nodiscard]] int indexInList(const MTP::ProxyData &value) const;
[[nodiscard]] QByteArray serialize() const;
bool setFromSerialized(const QByteArray &serialized);
@@ -47,14 +76,16 @@ public:
private:
bool _tryIPv6 = false;
bool _useProxyForCalls = false;
bool _proxyRotationEnabled = false;
bool _checkIpWarningShown = false;
int _proxyRotationTimeout = kDefaultProxyRotationTimeout;
MTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System;
MTP::ProxyData _selected;
std::vector<MTP::ProxyData> _list;
std::vector<int> _proxyRotationPreferredIndices;
rpl::event_stream<> _connectionTypeChanges;
};
} // namespace Core
@@ -0,0 +1,359 @@
/*
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 "core/proxy_rotation_manager.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "mtproto/facade.h"
#include <algorithm>
namespace Core {
namespace {
constexpr auto kProxyRotationCheckInterval = 2 * crl::time(1000);
constexpr auto kProxyRotationCheckLifetime = 20 * crl::time(1000);
constexpr auto kProxyRotationMaxActiveChecks = 10;
} // namespace
ProxyRotationManager::ProxyRotationManager()
: _checkTimer([=] { runChecks(); })
, _switchTimer([=] { switchTimerDone(); }) {
App().domain().accountsChanges(
) | rpl::on_next([=] {
stopChecking();
reevaluate();
}, _lifetime);
}
void ProxyRotationManager::settingsChanged() {
stopChecking();
pruneRemovedEntries();
reevaluate();
}
void ProxyRotationManager::handleConnectionStateChanged(
not_null<Main::Account*> account,
int32 state) {
(void)account;
(void)state;
reevaluate();
}
bool ProxyRotationManager::shouldObserve() const {
const auto &settings = App().settings().proxy();
return settings.isEnabled()
&& settings.selected()
&& settings.proxyRotationEnabled()
&& (settings.list().size() > 1);
}
std::vector<not_null<Main::Account*>> ProxyRotationManager::productionAccounts() const {
auto result = std::vector<not_null<Main::Account*>>();
for (const auto &entry : App().domain().accounts()) {
const auto account = entry.account.get();
if (!account->sessionExists() || account->mtp().isTestMode()) {
continue;
}
result.push_back(account);
}
return result;
}
not_null<Main::Account*> ProxyRotationManager::accountForChecks() const {
if (App().someSessionExists()
&& App().activeAccount().sessionExists()
&& !App().activeAccount().mtp().isTestMode()) {
return &App().activeAccount();
}
const auto accounts = productionAccounts();
Expects(!accounts.empty());
return accounts.front();
}
auto ProxyRotationManager::find(
const MTP::ProxyData &proxy) -> Entry* {
const auto i = ranges::find(
_entries,
proxy,
[](const Entry &entry) { return entry.proxy; });
return (i == end(_entries)) ? nullptr : &*i;
}
auto ProxyRotationManager::ensure(
const MTP::ProxyData &proxy) -> Entry& {
if (const auto entry = find(proxy)) {
return *entry;
}
_entries.push_back({ .proxy = proxy });
return _entries.back();
}
void ProxyRotationManager::reevaluate() {
if (!shouldObserve()) {
stopChecking();
return;
}
const auto accounts = productionAccounts();
if (accounts.empty()) {
stopChecking();
return;
}
const auto stateProj = [](not_null<Main::Account*> account) {
return account->mtp().dcstate();
};
if (ranges::contains(accounts, MTP::ConnectedState, stateProj)) {
stopChecking();
return;
}
startChecking();
}
void ProxyRotationManager::startChecking() {
if (_checking) {
return;
}
_checking = true;
_waitingToSwitch = false;
_switchStartedAt = crl::now();
updateProbeOrder();
runChecks();
const auto timeout = App().settings().proxy().proxyRotationTimeout();
_switchTimer.callOnce(timeout * crl::time(1000));
}
void ProxyRotationManager::stopChecking() {
_checkTimer.cancel();
_switchTimer.cancel();
_checking = false;
_waitingToSwitch = false;
_switchStartedAt = 0;
_probeOrder.clear();
_nextCheckIndex = 0;
clearPendingChecks();
}
void ProxyRotationManager::pruneRemovedEntries() {
const auto &settings = App().settings().proxy();
_entries.erase(
std::remove_if(begin(_entries), end(_entries), [&](const Entry &entry) {
return (settings.indexInList(entry.proxy) < 0);
}),
end(_entries));
}
void ProxyRotationManager::updateProbeOrder() {
const auto &settings = App().settings().proxy();
const auto currentIndex = settings.indexInList(settings.selected());
_probeOrder.clear();
_probeOrder.reserve(settings.list().size());
for (const auto index : settings.proxyRotationPreferredIndices()) {
if (index == currentIndex) {
continue;
}
_probeOrder.push_back(index);
}
for (auto i = 0, count = int(settings.list().size()); i != count; ++i) {
if (i == currentIndex || ranges::contains(_probeOrder, i)) {
continue;
}
_probeOrder.push_back(i);
}
_nextCheckIndex = 0;
}
void ProxyRotationManager::continueChecking(crl::time delay) {
if (!_checking) {
return;
}
if (_checkTimer.isActive()) {
_checkTimer.cancel();
}
_checkTimer.callOnce(delay);
}
void ProxyRotationManager::runChecks() {
if (!_checking) {
return;
}
if (!shouldObserve()) {
stopChecking();
return;
}
const auto accounts = productionAccounts();
if (accounts.empty()
|| ranges::contains(
accounts,
MTP::ConnectedState,
[](not_null<Main::Account*> account) {
return account->mtp().dcstate();
})) {
stopChecking();
return;
}
pruneExpiredChecks();
startNextCheck();
continueChecking(kProxyRotationCheckInterval);
}
void ProxyRotationManager::pruneExpiredChecks() {
const auto now = crl::now();
for (auto &entry : _entries) {
if (!entry.checking
|| (now - entry.startedAt < kProxyRotationCheckLifetime)) {
continue;
}
MTP::ResetProxyCheckers(entry.v4, entry.v6);
entry.checking = false;
entry.startedAt = 0;
}
}
void ProxyRotationManager::startNextCheck() {
if (_probeOrder.empty()) {
return;
}
if (ranges::count(_entries, true, &Entry::checking)
>= kProxyRotationMaxActiveChecks) {
return;
}
const auto &settings = App().settings().proxy();
auto attemptsLeft = int(_probeOrder.size());
while (attemptsLeft-- > 0) {
if (_nextCheckIndex >= int(_probeOrder.size())) {
_nextCheckIndex = 0;
}
const auto listIndex = _probeOrder[_nextCheckIndex++];
if (listIndex < 0 || listIndex >= int(settings.list().size())) {
continue;
}
const auto &proxy = settings.list()[listIndex];
auto &entry = ensure(proxy);
if (entry.checking) {
continue;
}
entry.checking = true;
entry.startedAt = crl::now();
MTP::StartProxyCheck(
&accountForChecks()->mtp(),
proxy,
settings.tryIPv6(),
entry.v4,
entry.v6,
[=](MTP::details::AbstractConnection *raw, int ping) {
checkDone(proxy, raw, ping);
},
[=](MTP::details::AbstractConnection *raw) {
checkFailed(proxy, raw);
});
break;
}
}
void ProxyRotationManager::switchTimerDone() {
if (!_checking || !shouldSwitchToAvailable()) {
return;
}
_waitingToSwitch = !switchToAvailable();
}
void ProxyRotationManager::clearPendingChecks() {
for (auto &entry : _entries) {
MTP::ResetProxyCheckers(entry.v4, entry.v6);
entry.checking = false;
entry.startedAt = 0;
}
}
void ProxyRotationManager::checkDone(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw,
int ping) {
const auto entry = find(proxy);
if (!entry
|| !entry->checking
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
return;
}
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
MTP::ResetProxyCheckers(entry->v4, entry->v6);
entry->checking = false;
entry->startedAt = 0;
entry->availableAt = crl::now();
const auto proxySettings = &App().settings().proxy();
if (const auto index = proxySettings->indexInList(proxy); index >= 0) {
if (proxySettings->promoteProxyRotationPreferredIndex(index)) {
App().saveSettingsDelayed();
}
}
updateProbeOrder();
if (_waitingToSwitch && shouldSwitchToAvailable()) {
_waitingToSwitch = !switchToAvailable();
}
}
void ProxyRotationManager::checkFailed(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw) {
const auto entry = find(proxy);
if (!entry
|| !entry->checking
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
return;
}
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
if (MTP::HasProxyCheckers(entry->v4, entry->v6)) {
return;
}
entry->checking = false;
entry->startedAt = 0;
}
bool ProxyRotationManager::switchToAvailable() {
if (!_checking) {
return false;
}
const auto &settings = App().settings().proxy();
for (const auto index : _probeOrder) {
if (index < 0 || index >= int(settings.list().size())) {
continue;
}
const auto &proxy = settings.list()[index];
const auto entry = find(proxy);
if (!entry || entry->checking || !entry->availableAt) {
continue;
}
if (entry->availableAt < _switchStartedAt) {
continue;
}
_waitingToSwitch = false;
App().setCurrentProxy(proxy, MTP::ProxyData::Settings::Enabled);
App().saveSettingsDelayed();
return true;
}
return false;
}
bool ProxyRotationManager::shouldSwitchToAvailable() const {
if (!_checking || !shouldObserve()) {
return false;
}
const auto accounts = productionAccounts();
return !accounts.empty()
&& !ranges::contains(
accounts,
MTP::ConnectedState,
[](not_null<Main::Account*> account) {
return account->mtp().dcstate();
});
}
} // namespace Core
@@ -0,0 +1,80 @@
/*
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/timer.h"
#include "mtproto/proxy_check.h"
#include <rpl/lifetime.h>
#include <vector>
namespace Main {
class Account;
} // namespace Main
namespace Core {
class ProxyRotationManager final {
public:
ProxyRotationManager();
void settingsChanged();
void handleConnectionStateChanged(
not_null<Main::Account*> account,
int32 state);
private:
struct Entry {
MTP::ProxyData proxy;
MTP::ProxyCheckConnection v4;
MTP::ProxyCheckConnection v6;
bool checking = false;
crl::time startedAt = 0;
crl::time availableAt = 0;
};
[[nodiscard]] bool shouldObserve() const;
[[nodiscard]] std::vector<not_null<Main::Account*>> productionAccounts() const;
[[nodiscard]] not_null<Main::Account*> accountForChecks() const;
[[nodiscard]] Entry *find(const MTP::ProxyData &proxy);
[[nodiscard]] Entry &ensure(const MTP::ProxyData &proxy);
void reevaluate();
void startChecking();
void stopChecking();
void pruneRemovedEntries();
void updateProbeOrder();
void continueChecking(crl::time delay);
void runChecks();
void pruneExpiredChecks();
void startNextCheck();
void switchTimerDone();
void clearPendingChecks();
void checkDone(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw,
int ping);
void checkFailed(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw);
[[nodiscard]] bool switchToAvailable();
[[nodiscard]] bool shouldSwitchToAvailable() const;
base::Timer _checkTimer;
base::Timer _switchTimer;
std::vector<Entry> _entries;
std::vector<int> _probeOrder;
int _nextCheckIndex = 0;
bool _checking = false;
bool _waitingToSwitch = false;
crl::time _switchStartedAt = 0;
rpl::lifetime _lifetime;
};
} // namespace Core
@@ -462,6 +462,7 @@ void Account::startMtp(std::unique_ptr<MTP::Config> config) {
_mtp->setStateChangedHandler([=](MTP::ShiftedDcId dc, int32 state) {
if (dc == _mtp->mainDcId()) {
Core::App().settings().proxy().connectionTypeChangesNotify();
Core::App().checkProxyRotation(this, state);
}
});
_mtp->setSessionResetHandler([=](MTP::ShiftedDcId shiftedDcId) {
@@ -0,0 +1,111 @@
/*
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/proxy_check.h"
#include "mtproto/facade.h"
#include "mtproto/mtproto_dc_options.h"
namespace MTP {
using Connection = details::AbstractConnection;
void ResetProxyCheckers(
ProxyCheckConnection &v4,
ProxyCheckConnection &v6) {
v4 = nullptr;
v6 = nullptr;
}
void DropProxyChecker(
ProxyCheckConnection &v4,
ProxyCheckConnection &v6,
not_null<Connection*> raw) {
if (v4.get() == raw) {
v4 = nullptr;
} else if (v6.get() == raw) {
v6 = nullptr;
}
}
bool HasProxyCheckers(
const ProxyCheckConnection &v4,
const ProxyCheckConnection &v6) {
return v4 || v6;
}
void StartProxyCheck(
not_null<Instance*> mtproto,
const ProxyData &proxy,
bool tryIPv6,
ProxyCheckConnection &v4,
ProxyCheckConnection &v6,
Fn<void(Connection *raw, int ping)> done,
Fn<void(Connection *raw)> fail) {
using Variants = DcOptions::Variants;
ResetProxyCheckers(v4, v6);
const auto connType = (proxy.type == ProxyData::Type::Http)
? Variants::Http
: Variants::Tcp;
const auto dcId = mtproto->mainDcId();
const auto setup = [&](ProxyCheckConnection &checker, const bytes::vector &secret) {
checker = Connection::Create(
mtproto,
connType,
QThread::currentThread(),
secret,
proxy);
const auto raw = checker.get();
raw->connect(raw, &Connection::connected, [=] {
if (done) {
done(raw, raw->pingTime());
}
});
const auto failed = [=] {
if (fail) {
fail(raw);
}
};
raw->connect(raw, &Connection::disconnected, failed);
raw->connect(raw, &Connection::error, failed);
};
if (proxy.type == ProxyData::Type::Mtproto) {
const auto secret = proxy.secretFromMtprotoPassword();
setup(v4, secret);
v4->connectToServer(
proxy.host,
proxy.port,
secret,
dcId,
false);
return;
}
const auto options = mtproto->dcOptions().lookup(
dcId,
DcType::Regular,
true);
const auto tryConnect = [&](ProxyCheckConnection &checker, Variants::Address address) {
const auto &list = options.data[address][connType];
if (list.empty() || ((address == Variants::IPv6) && !tryIPv6)) {
checker = nullptr;
return;
}
const auto &endpoint = list.front();
setup(checker, endpoint.secret);
checker->connectToServer(
QString::fromStdString(endpoint.ip),
endpoint.port,
endpoint.secret,
dcId,
false);
};
tryConnect(v4, Variants::IPv4);
tryConnect(v6, Variants::IPv6);
}
} // namespace MTP
@@ -0,0 +1,35 @@
/*
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/connection_abstract.h"
namespace MTP {
using ProxyCheckConnection = details::ConnectionPointer;
void ResetProxyCheckers(
ProxyCheckConnection &v4,
ProxyCheckConnection &v6);
void DropProxyChecker(
ProxyCheckConnection &v4,
ProxyCheckConnection &v6,
not_null<details::AbstractConnection*> raw);
[[nodiscard]] bool HasProxyCheckers(
const ProxyCheckConnection &v4,
const ProxyCheckConnection &v6);
void StartProxyCheck(
not_null<Instance*> mtproto,
const ProxyData &proxy,
bool tryIPv6,
ProxyCheckConnection &v4,
ProxyCheckConnection &v6,
Fn<void(details::AbstractConnection *raw, int ping)> done,
Fn<void(details::AbstractConnection *raw)> fail);
} // namespace MTP
@@ -487,9 +487,9 @@ bool ReadSetting(
proxySettings.setSettings(proxy
? MTP::ProxyData::Settings::Enabled
: MTP::ProxyData::Settings::System);
proxySettings.list() = proxy
? std::vector<MTP::ProxyData>{ 1, proxy }
: std::vector<MTP::ProxyData>{};
proxySettings.setList(proxy
? std::vector<MTP::ProxyData>{ proxy }
: std::vector<MTP::ProxyData>{});
Core::App().refreshGlobalProxy();
context.legacyRead = true;
} break;
@@ -562,16 +562,16 @@ bool ReadSetting(
if (!CheckStreamStatus(stream)) {
return false;
}
proxySettings.list() = list;
proxySettings.setList(std::move(list));
if (connectionType == dbictProxiesListOld) {
settings = static_cast<qint32>(
(index > 0 && index <= list.size()
(index > 0 && index <= proxySettings.list().size()
? MTP::ProxyData::Settings::Enabled
: MTP::ProxyData::Settings::System));
index = std::abs(index);
}
proxySettings.setSelected((index > 0 && index <= list.size())
? list[index - 1]
proxySettings.setSelected((index > 0 && index <= proxySettings.list().size())
? proxySettings.list()[index - 1]
: MTP::ProxyData());
const auto unchecked = static_cast<MTP::ProxyData::Settings>(settings);
@@ -596,14 +596,14 @@ bool ReadSetting(
return false;
}
if (proxy) {
proxySettings.list() = { 1, proxy };
proxySettings.setList({ proxy });
proxySettings.setSelected(proxy);
proxySettings.setSettings((connectionType == dbictTcpProxy
|| connectionType == dbictHttpProxy)
? MTP::ProxyData::Settings::Enabled
: MTP::ProxyData::Settings::System);
} else {
proxySettings.list() = {};
proxySettings.setList({});
proxySettings.setSelected(MTP::ProxyData());
proxySettings.setSettings(MTP::ProxyData::Settings::System);
}