| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- /*
- 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_domain_resolver.h"
- #include "base/random.h"
- #include "base/invoke_queued.h"
- #include "base/call_delayed.h"
- #include <QtCore/QJsonDocument>
- #include <QtCore/QJsonArray>
- #include <QtCore/QJsonObject>
- #include <range/v3/algorithm/shuffle.hpp>
- #include <range/v3/algorithm/reverse.hpp>
- #include <range/v3/algorithm/remove.hpp>
- #include <random>
- namespace MTP::details {
- namespace {
- constexpr auto kSendNextTimeout = crl::time(800);
- constexpr auto kMinTimeToLive = 10 * crl::time(1000);
- constexpr auto kMaxTimeToLive = 300 * crl::time(1000);
- } // namespace
- const std::vector<QString> &DnsDomains() {
- static const auto kResult = std::vector<QString>{
- "google.com",
- "www.google.com",
- "google.ru",
- "www.google.ru",
- };
- return kResult;
- }
- QString GenerateDnsRandomPadding() {
- constexpr char kValid[] = "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- auto result = QString();
- const auto count = [&] {
- constexpr auto kMinPadding = 13;
- constexpr auto kMaxPadding = 128;
- while (true) {
- const auto result = 1 + (base::RandomValue<uchar>() / 2);
- Assert(result <= kMaxPadding);
- if (result >= kMinPadding) {
- return result;
- }
- }
- }();
- result.resize(count);
- for (auto &ch : result) {
- ch = kValid[base::RandomValue<uchar>() % (sizeof(kValid) - 1)];
- }
- return result;
- }
- QByteArray DnsUserAgent() {
- static const auto kResult = QByteArray(
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
- "AppleWebKit/537.36 (KHTML, like Gecko) "
- "Chrome/133.0.0.0 Safari/537.36");
- return kResult;
- }
- std::vector<DnsEntry> ParseDnsResponse(
- const QByteArray &bytes,
- std::optional<int> typeRestriction) {
- if (bytes.isEmpty()) {
- return {};
- }
- // Read and store to "result" all the data bytes from the response:
- // { ..,
- // "Answer": [
- // { .., "data": "bytes1", "TTL": int, .. },
- // { .., "data": "bytes2", "TTL": int, .. }
- // ],
- // .. }
- auto error = QJsonParseError{ 0, QJsonParseError::NoError };
- const auto document = QJsonDocument::fromJson(bytes, &error);
- if (error.error != QJsonParseError::NoError) {
- LOG(("Config Error: Failed to parse dns response JSON, error: %1"
- ).arg(error.errorString()));
- return {};
- } else if (!document.isObject()) {
- LOG(("Config Error: Not an object received in dns response JSON."));
- return {};
- }
- const auto response = document.object();
- const auto answerIt = response.find("Answer");
- if (answerIt == response.constEnd()) {
- LOG(("Config Error: Could not find Answer in dns response JSON."));
- return {};
- } else if (!(*answerIt).isArray()) {
- LOG(("Config Error: Not an array received "
- "in Answer in dns response JSON."));
- return {};
- }
- const auto array = (*answerIt).toArray();
- auto result = std::vector<DnsEntry>();
- for (const auto elem : array) {
- if (!elem.isObject()) {
- LOG(("Config Error: Not an object found "
- "in Answer array in dns response JSON."));
- continue;
- }
- const auto object = elem.toObject();
- if (typeRestriction) {
- const auto typeIt = object.find("type");
- const auto type = int(base::SafeRound((*typeIt).toDouble()));
- if (!(*typeIt).isDouble()) {
- LOG(("Config Error: Not a number in type field "
- "in Answer array in dns response JSON."));
- continue;
- } else if (type != *typeRestriction) {
- continue;
- }
- }
- const auto dataIt = object.find("data");
- if (dataIt == object.constEnd()) {
- LOG(("Config Error: Could not find data "
- "in Answer array entry in dns response JSON."));
- continue;
- } else if (!(*dataIt).isString()) {
- LOG(("Config Error: Not a string data found "
- "in Answer array entry in dns response JSON."));
- continue;
- }
- const auto ttlIt = object.find("TTL");
- const auto ttl = (ttlIt != object.constEnd())
- ? crl::time(base::SafeRound((*ttlIt).toDouble()))
- : crl::time(0);
- result.push_back({ (*dataIt).toString(), ttl });
- }
- return result;
- }
- ServiceWebRequest::ServiceWebRequest(not_null<QNetworkReply*> reply)
- : reply(reply.get()) {
- }
- ServiceWebRequest::ServiceWebRequest(ServiceWebRequest &&other)
- : reply(base::take(other.reply)) {
- }
- ServiceWebRequest &ServiceWebRequest::operator=(ServiceWebRequest &&other) {
- if (reply != other.reply) {
- destroy();
- reply = base::take(other.reply);
- }
- return *this;
- }
- void ServiceWebRequest::destroy() {
- if (const auto value = base::take(reply)) {
- value->disconnect(
- value,
- &QNetworkReply::finished,
- nullptr,
- nullptr);
- value->abort();
- value->deleteLater();
- }
- }
- ServiceWebRequest::~ServiceWebRequest() {
- if (reply) {
- reply->deleteLater();
- }
- }
- DomainResolver::DomainResolver(Fn<void(
- const QString &host,
- const QStringList &ips,
- crl::time expireAt)> callback)
- : _callback(std::move(callback)) {
- _manager.setProxy(QNetworkProxy::NoProxy);
- }
- void DomainResolver::resolve(const QString &domain) {
- resolve({ domain, false });
- resolve({ domain, true });
- }
- void DomainResolver::resolve(const AttemptKey &key) {
- if (_attempts.find(key) != end(_attempts)) {
- return;
- } else if (_requests.find(key) != end(_requests)) {
- return;
- }
- const auto i = _cache.find(key);
- _lastTimestamp = crl::now();
- if (i != end(_cache) && i->second.expireAt > _lastTimestamp) {
- checkExpireAndPushResult(key.domain);
- return;
- }
- auto attempts = std::vector<Attempt>();
- auto domains = DnsDomains();
- std::random_device rd;
- ranges::shuffle(domains, std::mt19937(rd()));
- const auto takeDomain = [&] {
- const auto result = domains.back();
- domains.pop_back();
- return result;
- };
- const auto shuffle = [&](int from, int till) {
- Expects(till > from);
- ranges::shuffle(
- begin(attempts) + from,
- begin(attempts) + till,
- std::mt19937(rd()));
- };
- attempts.push_back({ Type::Google, "dns.google.com" });
- attempts.push_back({ Type::Google, takeDomain(), "dns" });
- attempts.push_back({ Type::Mozilla, "mozilla.cloudflare-dns.com" });
- while (!domains.empty()) {
- attempts.push_back({ Type::Google, takeDomain(), "dns" });
- }
- shuffle(0, 2);
- ranges::reverse(attempts); // We go from last to first.
- _attempts.emplace(key, Attempts{ std::move(attempts) });
- sendNextRequest(key);
- }
- void DomainResolver::checkExpireAndPushResult(const QString &domain) {
- const auto ipv4 = _cache.find({ domain, false });
- if (ipv4 == end(_cache) || ipv4->second.expireAt <= _lastTimestamp) {
- return;
- }
- auto result = ipv4->second;
- const auto ipv6 = _cache.find({ domain, true });
- if (ipv6 != end(_cache) && ipv6->second.expireAt > _lastTimestamp) {
- result.ips.append(ipv6->second.ips);
- accumulate_min(result.expireAt, ipv6->second.expireAt);
- }
- InvokeQueued(this, [=] {
- _callback(domain, result.ips, result.expireAt);
- });
- }
- void DomainResolver::sendNextRequest(const AttemptKey &key) {
- auto i = _attempts.find(key);
- if (i == end(_attempts)) {
- return;
- }
- auto &attempts = i->second;
- auto &list = attempts.list;
- const auto attempt = list.back();
- list.pop_back();
- if (!list.empty()) {
- base::call_delayed(kSendNextTimeout, &attempts.guard, [=] {
- sendNextRequest(key);
- });
- }
- performRequest(key, attempt);
- }
- void DomainResolver::performRequest(
- const AttemptKey &key,
- const Attempt &attempt) {
- auto url = QUrl();
- url.setScheme("https");
- auto request = QNetworkRequest();
- switch (attempt.type) {
- case Type::Mozilla: {
- url.setHost(attempt.data);
- url.setPath("/dns-query");
- url.setQuery(QStringLiteral("name=%1&type=%2&random_padding=%3"
- ).arg(key.domain
- ).arg(key.ipv6 ? 28 : 1
- ).arg(GenerateDnsRandomPadding()));
- request.setRawHeader("accept", "application/dns-json");
- } break;
- case Type::Google: {
- url.setHost(attempt.data);
- url.setPath("/resolve");
- url.setQuery(QStringLiteral("name=%1&type=%2&random_padding=%3"
- ).arg(key.domain
- ).arg(key.ipv6 ? 28 : 1
- ).arg(GenerateDnsRandomPadding()));
- if (!attempt.host.isEmpty()) {
- const auto host = attempt.host + ".google.com";
- request.setRawHeader("Host", host.toLatin1());
- }
- } break;
- default: Unexpected("Type in DomainResolver::performRequest.");
- }
- request.setUrl(url);
- request.setRawHeader("User-Agent", DnsUserAgent());
- const auto i = _requests.emplace(
- key,
- std::vector<ServiceWebRequest>()).first;
- const auto reply = i->second.emplace_back(
- _manager.get(request)
- ).reply;
- connect(reply, &QNetworkReply::finished, this, [=] {
- requestFinished(key, reply);
- });
- }
- void DomainResolver::requestFinished(
- const AttemptKey &key,
- not_null<QNetworkReply*> reply) {
- const auto result = finalizeRequest(key, reply);
- const auto response = ParseDnsResponse(result);
- if (response.empty()) {
- return;
- }
- _requests.erase(key);
- _attempts.erase(key);
- auto entry = CacheEntry();
- auto ttl = kMaxTimeToLive;
- for (const auto &item : response) {
- entry.ips.push_back(item.data);
- ttl = std::min(
- ttl,
- std::max(item.TTL * crl::time(1000), kMinTimeToLive));
- }
- _lastTimestamp = crl::now();
- entry.expireAt = _lastTimestamp + ttl;
- _cache[key] = std::move(entry);
- checkExpireAndPushResult(key.domain);
- }
- QByteArray DomainResolver::finalizeRequest(
- const AttemptKey &key,
- not_null<QNetworkReply*> reply) {
- if (reply->error() != QNetworkReply::NoError) {
- DEBUG_LOG(("Resolve Error: Failed to get response, error: %2 (%3)"
- ).arg(reply->errorString()
- ).arg(reply->error()));
- }
- const auto result = reply->readAll();
- const auto i = _requests.find(key);
- if (i != end(_requests)) {
- auto &requests = i->second;
- const auto from = ranges::remove(
- requests,
- reply,
- [](const ServiceWebRequest &request) { return request.reply; });
- requests.erase(from, end(requests));
- if (requests.empty()) {
- _requests.erase(i);
- }
- }
- return result;
- }
- } // namespace MTP::details
|