ConfigRecoverer: support tls obfuscation

GitOrigin-RevId: 9a924ee86fe042a9fd1acf44eeeb25c0ab4e8bbb
This commit is contained in:
Arseny Smirnov 2019-07-11 13:25:20 +02:00
parent 64517c9165
commit 8fdd8910e7
4 changed files with 206 additions and 165 deletions

View File

@ -53,9 +53,85 @@
#include <memory>
#include <utility>
#include <ctime>
namespace td {
int VERBOSITY_NAME(config_recoverer) = VERBOSITY_NAME(INFO);
Result<int64> HttpDate::to_unix_time(int32 year, int32 month, int32 day, int32 hour, int32 minute, int32 second) {
int64 res = 0;
if (year < 1970 || year > 2037) {
return td::Status::Error("invalid year");
}
if (month < 1 || month > 12) {
return td::Status::Error("invalid month");
}
if (day < 1 || day > days_in_month(year, month)) {
return td::Status::Error("invalid day");
}
if (hour < 0 || hour > 24) { // is hour == 24 possible?
return td::Status::Error("invalid hour");
}
if (minute < 0 || minute > 60) {
return td::Status::Error("invalid minute");
}
if (second < 0 || second > 60) {
return td::Status::Error("invalid second");
}
for (int y = 1970; y < year; y++) {
res += (is_leap(y) + 365) * seconds_in_day();
}
for (int m = 1; m < month; m++) {
res += days_in_month(year, m) * seconds_in_day();
}
res += (day - 1) * seconds_in_day();
res += hour * 60 * 60;
res += minute * 60;
res += second;
return res;
}
Result<int64> HttpDate::parse_http_date(std::string slice) {
td::Parser p(slice);
p.read_till(','); // ignore week day
p.skip(',');
p.skip_whitespaces();
TRY_RESULT(day, to_integer_safe<int32>(p.read_word()));
auto month_name = p.read_word();
to_lower_inplace(month_name);
TRY_RESULT(year, to_integer_safe<int32>(p.read_word()));
p.skip_whitespaces();
p.skip_nofail('0');
TRY_RESULT(hour, to_integer_safe<int32>(p.read_till(':')));
p.skip(':');
p.skip_nofail('0');
TRY_RESULT(minute, to_integer_safe<int32>(p.read_till(':')));
p.skip(':');
p.skip_nofail('0');
TRY_RESULT(second, to_integer_safe<int32>(p.read_word()));
auto gmt = p.read_word();
TRY_STATUS(std::move(p.status()));
if (gmt != "GMT") {
return Status::Error("timezone must be GMT");
}
Slice month_names[12] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"};
int month = 0;
for (int m = 1; m <= 12; m++) {
if (month_names[m - 1] == month_name) {
month = m;
break;
}
}
if (month == 0) {
return Status::Error("Unknown month name");
}
return HttpDate::to_unix_time(year, month, day, hour, minute, second);
}
Result<SimpleConfig> decode_config(Slice input) {
static auto rsa = td::RSA::from_pem(
@ -117,8 +193,8 @@ Result<SimpleConfig> decode_config(Slice input) {
return std::move(config);
}
static ActorOwn<> get_simple_config_impl(Promise<SimpleConfig> promise, int32 scheduler_id, string url, string host,
bool prefer_ipv6) {
static ActorOwn<> get_simple_config_impl(Promise<SimpleConfigResult> promise, int32 scheduler_id, string url,
string host, bool prefer_ipv6) {
VLOG(config_recoverer) << "Request simple config from " << url;
#if TD_EMSCRIPTEN // FIXME
return ActorOwn<>();
@ -128,9 +204,12 @@ static ActorOwn<> get_simple_config_impl(Promise<SimpleConfig> promise, int32 sc
return ActorOwn<>(create_actor_on_scheduler<Wget>(
"Wget", scheduler_id,
PromiseCreator::lambda([promise = std::move(promise)](Result<unique_ptr<HttpQuery>> r_query) mutable {
promise.set_result([&]() -> Result<SimpleConfig> {
promise.set_result([&]() -> Result<SimpleConfigResult> {
TRY_RESULT(http_query, std::move(r_query));
return decode_config(http_query->content_);
SimpleConfigResult res;
res.r_http_date = HttpDate::parse_http_date(http_query->get_arg("date").str());
res.r_config = decode_config(http_query->content_);
return res;
}());
}),
std::move(url), std::vector<std::pair<string, string>>({{"Host", std::move(host)}}), timeout, ttl, prefer_ipv6,
@ -138,7 +217,7 @@ static ActorOwn<> get_simple_config_impl(Promise<SimpleConfig> promise, int32 sc
#endif
}
ActorOwn<> get_simple_config_azure(Promise<SimpleConfig> promise, const ConfigShared *shared_config, bool is_test,
ActorOwn<> get_simple_config_azure(Promise<SimpleConfigResult> promise, const ConfigShared *shared_config, bool is_test,
int32 scheduler_id) {
string url = PSTRING() << "https://software-download.microsoft.com/" << (is_test ? "test" : "prod")
<< "v2/config.txt";
@ -146,8 +225,8 @@ ActorOwn<> get_simple_config_azure(Promise<SimpleConfig> promise, const ConfigSh
return get_simple_config_impl(std::move(promise), scheduler_id, std::move(url), "tcdnb.azureedge.net", prefer_ipv6);
}
ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfig> promise, const ConfigShared *shared_config, bool is_test,
int32 scheduler_id) {
ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfigResult> promise, const ConfigShared *shared_config,
bool is_test, int32 scheduler_id) {
VLOG(config_recoverer) << "Request simple config from Google DNS";
#if TD_EMSCRIPTEN // FIXME
return ActorOwn<>();
@ -156,40 +235,47 @@ ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfig> promise, const Con
const int timeout = 10;
const int ttl = 3;
const bool prefer_ipv6 = shared_config == nullptr ? false : shared_config->get_option_boolean("prefer_ipv6");
if (name.empty()) {
name = is_test ? "tapv2.stel.com" : "apv2.stel.com";
if (name.empty() || true) {
name = is_test ? "tapv3.stel.com" : "apv3.stel.com";
}
return ActorOwn<>(create_actor_on_scheduler<Wget>(
"Wget", scheduler_id,
PromiseCreator::lambda([promise = std::move(promise)](Result<unique_ptr<HttpQuery>> r_query) mutable {
promise.set_result([&]() -> Result<SimpleConfig> {
promise.set_result([&]() -> Result<SimpleConfigResult> {
TRY_RESULT(http_query, std::move(r_query));
TRY_RESULT(json, json_decode(http_query->content_));
if (json.type() != JsonValue::Type::Object) {
return Status::Error("json error");
}
auto &answer_object = json.get_object();
TRY_RESULT(answer, get_json_object_field(answer_object, "Answer", JsonValue::Type::Array, false));
auto &answer_array = answer.get_array();
vector<string> parts;
for (auto &v : answer_array) {
if (v.type() != JsonValue::Type::Object) {
LOG(ERROR) << *http_query;
SimpleConfigResult res;
res.r_http_date = HttpDate::parse_http_date(http_query->get_arg("date").str());
res.r_config = [&]() -> Result<SimpleConfig> {
TRY_RESULT(json, json_decode(http_query->content_));
if (json.type() != JsonValue::Type::Object) {
return Status::Error("json error");
}
auto &data_object = v.get_object();
TRY_RESULT(part, get_json_object_string_field(data_object, "data", false));
parts.push_back(std::move(part));
}
if (parts.size() != 2) {
return Status::Error("Expected data in two parts");
}
string data;
if (parts[0].size() < parts[1].size()) {
data = parts[1] + parts[0];
} else {
data = parts[0] + parts[1];
}
return decode_config(data);
auto &answer_object = json.get_object();
TRY_RESULT(answer, get_json_object_field(answer_object, "Answer", JsonValue::Type::Array, false));
auto &answer_array = answer.get_array();
vector<string> parts;
for (auto &v : answer_array) {
if (v.type() != JsonValue::Type::Object) {
return Status::Error("json error");
}
auto &data_object = v.get_object();
TRY_RESULT(part, get_json_object_string_field(data_object, "data", false));
parts.push_back(std::move(part));
}
if (parts.size() != 2) {
return Status::Error("Expected data in two parts");
}
string data;
if (parts[0].size() < parts[1].size()) {
data = parts[1] + parts[0];
} else {
data = parts[0] + parts[1];
}
return decode_config(data);
}();
return res;
}());
}),
PSTRING() << "https://www.google.com/resolve?name=" << url_encode(name) << "&type=16",
@ -198,11 +284,10 @@ ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfig> promise, const Con
#endif
}
ActorOwn<> get_full_config(DcId dc_id, IPAddress ip_address, mtproto::ProxySecret secret, Promise<FullConfig> promise) {
ActorOwn<> get_full_config(DcOption option, Promise<FullConfig> promise) {
class SessionCallback : public Session::Callback {
public:
SessionCallback(ActorShared<> parent, IPAddress address, mtproto::ProxySecret secret)
: parent_(std::move(parent)), address_(std::move(address)), secret_(std::move(secret)) {
SessionCallback(ActorShared<> parent, DcOption option) : parent_(std::move(parent)), option_(std::move(option)) {
}
void on_failed() final {
}
@ -211,10 +296,14 @@ ActorOwn<> get_full_config(DcId dc_id, IPAddress ip_address, mtproto::ProxySecre
void request_raw_connection(unique_ptr<mtproto::AuthData> auth_data,
Promise<unique_ptr<mtproto::RawConnection>> promise) final {
request_raw_connection_cnt_++;
VLOG(config_recoverer) << "Request full config from " << address_ << ", try = " << request_raw_connection_cnt_;
VLOG(config_recoverer) << "Request full config from " << option_.get_ip_address()
<< ", try = " << request_raw_connection_cnt_;
if (request_raw_connection_cnt_ <= 2) {
send_closure(G()->connection_creator(), &ConnectionCreator::request_raw_connection_by_ip, address_,
mtproto::TransportType{mtproto::TransportType::ObfuscatedTcp, 0, secret_}, std::move(promise));
send_closure(G()->connection_creator(), &ConnectionCreator::request_raw_connection_by_ip,
option_.get_ip_address(),
mtproto::TransportType{mtproto::TransportType::ObfuscatedTcp,
narrow_cast<int16>(option_.get_dc_id().get_raw_id()), option_.get_secret()},
std::move(promise));
} else {
// Delay all queries except first forever
delay_forever_.push_back(std::move(promise));
@ -229,8 +318,7 @@ ActorOwn<> get_full_config(DcId dc_id, IPAddress ip_address, mtproto::ProxySecre
private:
ActorShared<> parent_;
IPAddress address_;
mtproto::ProxySecret secret_;
DcOption option_;
size_t request_raw_connection_cnt_{0};
std::vector<Promise<unique_ptr<mtproto::RawConnection>>> delay_forever_;
};
@ -311,17 +399,16 @@ ActorOwn<> get_full_config(DcId dc_id, IPAddress ip_address, mtproto::ProxySecre
class GetConfigActor : public NetQueryCallback {
public:
GetConfigActor(DcId dc_id, IPAddress ip_address, mtproto::ProxySecret secret, Promise<FullConfig> promise)
: dc_id_(dc_id), ip_address_(std::move(ip_address)), secret_(std::move(secret)), promise_(std::move(promise)) {
GetConfigActor(DcOption option, Promise<FullConfig> promise)
: option_(std::move(option)), promise_(std::move(promise)) {
}
private:
void start_up() override {
auto session_callback =
make_unique<SessionCallback>(actor_shared(this, 1), std::move(ip_address_), std::move(secret_));
auto auth_data = std::make_shared<SimpleAuthData>(option_.get_dc_id());
int32 int_dc_id = option_.get_dc_id().get_raw_id();
auto session_callback = make_unique<SessionCallback>(actor_shared(this, 1), std::move(option_));
auto auth_data = std::make_shared<SimpleAuthData>(dc_id_);
int32 int_dc_id = dc_id_.get_raw_id();
if (G()->is_test_dc()) {
int_dc_id += 10000;
}
@ -357,15 +444,12 @@ ActorOwn<> get_full_config(DcId dc_id, IPAddress ip_address, mtproto::ProxySecre
session_.reset();
}
DcId dc_id_;
IPAddress ip_address_;
DcOption option_;
ActorOwn<Session> session_;
mtproto::ProxySecret secret_;
Promise<FullConfig> promise_;
};
return ActorOwn<>(create_actor<GetConfigActor>("GetConfigActor", dc_id, std::move(ip_address), std::move(secret),
std::move(promise)));
return ActorOwn<>(create_actor<GetConfigActor>("GetConfigActor", option, std::move(promise)));
}
class ConfigRecoverer : public Actor {
@ -434,9 +518,31 @@ class ConfigRecoverer : public Actor {
return found;
}
void on_simple_config(Result<SimpleConfig> r_simple_config, bool dummy) {
void on_simple_config(Result<SimpleConfigResult> r_simple_config_result, bool dummy) {
simple_config_query_.reset();
dc_options_i_ = 0;
SimpleConfigResult cfg;
if (r_simple_config_result.is_error()) {
cfg.r_http_date = r_simple_config_result.error().clone();
cfg.r_config = r_simple_config_result.move_as_error();
} else {
cfg = r_simple_config_result.move_as_ok();
}
if (cfg.r_http_date.is_ok() && (date_option_i_ == 0 || cfg.r_config.is_error())) {
G()->update_dns_time_difference(cfg.r_http_date.ok() - Time::now());
} else if (cfg.r_config.is_ok()) {
G()->update_dns_time_difference(cfg.r_config.ok()->date_ - Time::now());
}
date_option_i_ = (date_option_i_ + 1) % 2;
do_on_simple_config(std::move(cfg.r_config));
update_dc_options();
loop();
}
void do_on_simple_config(Result<SimpleConfig> r_simple_config) {
if (r_simple_config.is_ok()) {
auto config = r_simple_config.move_as_ok();
VLOG(config_recoverer) << "Receive raw " << to_string(config);
@ -460,7 +566,6 @@ class ConfigRecoverer : public Actor {
VLOG(config_recoverer) << "Config has expired at " << config->expires_;
}
G()->update_dns_time_difference(config->date_ - Time::now());
simple_config_expires_at_ = get_config_expire_time();
simple_config_at_ = Time::now_cached();
for (size_t i = 1; i < simple_config_.dc_options.size(); i++) {
@ -471,8 +576,6 @@ class ConfigRecoverer : public Actor {
simple_config_ = DcOptions();
simple_config_expires_at_ = get_failed_config_expire_time();
}
update_dc_options();
loop();
}
void on_full_config(Result<FullConfig> r_full_config, bool dummy) {
@ -526,6 +629,8 @@ class ConfigRecoverer : public Actor {
double dc_options_at_{0};
size_t dc_options_i_;
size_t date_option_i_{0};
FullConfig full_config_;
double full_config_expires_at_{0};
ActorOwn<> full_config_query_;
@ -593,9 +698,10 @@ class ConfigRecoverer : public Actor {
if (need_simple_config) {
ref_cnt_++;
VLOG(config_recoverer) << "ASK SIMPLE CONFIG";
auto promise = PromiseCreator::lambda([actor_id = actor_shared(this)](Result<SimpleConfig> r_simple_config) {
send_closure(actor_id, &ConfigRecoverer::on_simple_config, std::move(r_simple_config), false);
});
auto promise =
PromiseCreator::lambda([actor_id = actor_shared(this)](Result<SimpleConfigResult> r_simple_config) {
send_closure(actor_id, &ConfigRecoverer::on_simple_config, std::move(r_simple_config), false);
});
auto get_simple_config = [&]() {
switch (simple_config_turn_ % 3) {
case 1:
@ -614,12 +720,11 @@ class ConfigRecoverer : public Actor {
if (need_full_config) {
ref_cnt_++;
VLOG(config_recoverer) << "ASK FULL CONFIG";
full_config_query_ = get_full_config(
dc_options_.dc_options[dc_options_i_].get_dc_id(), dc_options_.dc_options[dc_options_i_].get_ip_address(),
dc_options_.dc_options[dc_options_i_].get_secret(),
PromiseCreator::lambda([actor_id = actor_shared(this)](Result<FullConfig> r_full_config) {
send_closure(actor_id, &ConfigRecoverer::on_full_config, std::move(r_full_config), false);
}));
full_config_query_ =
get_full_config(dc_options_.dc_options[dc_options_i_],
PromiseCreator::lambda([actor_id = actor_shared(this)](Result<FullConfig> r_full_config) {
send_closure(actor_id, &ConfigRecoverer::on_full_config, std::move(r_full_config), false);
}));
dc_options_i_ = (dc_options_i_ + 1) % dc_options_.dc_options.size();
}

View File

@ -28,14 +28,34 @@ extern int VERBOSITY_NAME(config_recoverer);
class ConfigShared;
using SimpleConfig = tl_object_ptr<telegram_api::help_configSimple>;
struct SimpleConfigResult {
Result<SimpleConfig> r_config;
Result<int64> r_http_date;
};
Result<SimpleConfig> decode_config(Slice input);
ActorOwn<> get_simple_config_azure(Promise<SimpleConfig> promise, const ConfigShared *shared_config, bool is_test,
ActorOwn<> get_simple_config_azure(Promise<SimpleConfigResult> promise, const ConfigShared *shared_config, bool is_test,
int32 scheduler_id);
ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfig> promise, const ConfigShared *shared_config, bool is_test,
int32 scheduler_id);
ActorOwn<> get_simple_config_google_dns(Promise<SimpleConfigResult> promise, const ConfigShared *shared_config,
bool is_test, int32 scheduler_id);
class HttpDate {
public:
static bool is_leap(int year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
static int32 days_in_month(int year, int month) {
static int cnt[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
return cnt[month - 1] + (month == 2 && is_leap(year));
}
static int64 seconds_in_day() {
return 24 * 60 * 60;
}
static Result<int64> to_unix_time(int32 year, int32 month, int32 day, int32 hour, int32 minute, int32 second);
static Result<int64> parse_http_date(std::string slice);
};
using FullConfig = tl_object_ptr<telegram_api::config>;

View File

@ -753,6 +753,9 @@ void ConnectionCreator::request_raw_connection_by_ip(IPAddress ip_address, mtpro
};
auto token = next_token();
auto callback = td::make_unique<Callback>(std::move(socket_fd_promise));
LOG(INFO) << "Tls in ConfigRecoverer " << ip_address << " " << transport_type.secret.get_domain() << " "
<< transport_type.secret.emulate_tls() << " " << transport_type.secret.get_proxy_secret().size()
<< transport_type.secret.get_encoded_secret();
children_[token] = {false, create_actor<mtproto::TlsInit>(
"TlsInit", std::move(socket_fd), ip_address, transport_type.secret.get_domain(),
transport_type.secret.get_proxy_secret().str(), std::move(callback),
@ -984,7 +987,8 @@ void ConnectionCreator::client_loop(ClientInfo &client) {
bool was_connected_{false};
unique_ptr<detail::StatsCallback> stats_callback_;
};
LOG(INFO) << "Start " << (proxy.use_socks5_proxy() ? "Socks5" : "HTTP") << ": " << extra.debug_str;
LOG(INFO) << "Start " << (proxy.use_socks5_proxy() ? "Socks5" : (proxy.use_http_tcp_proxy() ? "HTTP" : "Tls"))
<< ": " << extra.debug_str;
auto token = next_token();
auto callback = td::make_unique<Callback>(std::move(promise), std::move(stats_callback));
if (proxy.use_socks5_proxy()) {

View File

@ -121,105 +121,17 @@ TEST(Mtproto, GetHostByNameActor) {
sched.finish();
}
class Date {
public:
static bool is_leap(int year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
static int32 days_in_month(int year, int month) {
int cnt[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
return cnt[month - 1] + (month == 2 && is_leap(year));
}
static int64 seconds_in_day() {
return 24 * 60 * 60;
}
static Result<int64> to_unix_time(int32 year, int32 month, int32 day, int32 hour, int32 minute, int32 second) {
int64 res = 0;
if (year < 1970 || year > 2037) {
return td::Status::Error("invalid year");
}
if (month < 1 || month > 12) {
return td::Status::Error("invalid month");
}
if (day < 1 || day > days_in_month(year, month)) {
return td::Status::Error("invalid day");
}
if (hour < 0 || hour > 24) { // is hour == 24 possible?
return td::Status::Error("invalid hour");
}
if (minute < 0 || minute > 60) {
return td::Status::Error("invalid minute");
}
if (second < 0 || second > 60) {
return td::Status::Error("invalid second");
}
for (int y = 1970; y < year; y++) {
res += (is_leap(y) + 365) * seconds_in_day();
}
for (int m = 1; m < month; m++) {
res += days_in_month(year, m) * seconds_in_day();
}
res += (day - 1) * seconds_in_day();
res += hour * 60 * 60;
res += minute * 60;
res += second;
return res;
}
};
TEST(Time, to_unix_time) {
ASSERT_EQ(0, Date::to_unix_time(1970, 1, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(60 * 60 + 60 + 1, Date::to_unix_time(1970, 1, 1, 1, 1, 1).move_as_ok());
ASSERT_EQ(24 * 60 * 60, Date::to_unix_time(1970, 1, 2, 0, 0, 0).move_as_ok());
ASSERT_EQ(31 * 24 * 60 * 60, Date::to_unix_time(1970, 2, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(365 * 24 * 60 * 60, Date::to_unix_time(1971, 1, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(1562780559, Date::to_unix_time(2019, 7, 10, 17, 42, 39).move_as_ok());
}
Result<int64> parse_http_date(std::string slice) {
td::Parser p(slice);
p.read_till(','); // ignore week day
p.skip(',');
p.skip_whitespaces();
TRY_RESULT(day, to_integer_safe<int32>(p.read_word()));
auto month_name = p.read_word();
to_lower_inplace(month_name);
TRY_RESULT(year, to_integer_safe<int32>(p.read_word()));
p.skip_whitespaces();
p.skip_nofail('0');
TRY_RESULT(hour, to_integer_safe<int32>(p.read_till(':')));
p.skip(':');
p.skip_nofail('0');
TRY_RESULT(minute, to_integer_safe<int32>(p.read_till(':')));
p.skip(':');
p.skip_nofail('0');
TRY_RESULT(second, to_integer_safe<int32>(p.read_word()));
auto gmt = p.read_word();
TRY_STATUS(std::move(p.status()));
if (gmt != "GMT") {
return Status::Error("timezone must be GMT");
}
Slice month_names[12] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"};
int month = 0;
for (int m = 1; m <= 12; m++) {
if (month_names[m - 1] == month_name) {
month = m;
break;
}
}
if (month == 0) {
return Status::Error("Unknown month name");
}
return Date::to_unix_time(year, month, day, hour, minute, second);
ASSERT_EQ(0, HttpDate::to_unix_time(1970, 1, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(60 * 60 + 60 + 1, HttpDate::to_unix_time(1970, 1, 1, 1, 1, 1).move_as_ok());
ASSERT_EQ(24 * 60 * 60, HttpDate::to_unix_time(1970, 1, 2, 0, 0, 0).move_as_ok());
ASSERT_EQ(31 * 24 * 60 * 60, HttpDate::to_unix_time(1970, 2, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(365 * 24 * 60 * 60, HttpDate::to_unix_time(1971, 1, 1, 0, 0, 0).move_as_ok());
ASSERT_EQ(1562780559, HttpDate::to_unix_time(2019, 7, 10, 17, 42, 39).move_as_ok());
}
TEST(Time, parse_http_date) {
ASSERT_EQ(784887151, parse_http_date("Tue, 15 Nov 1994 08:12:31 GMT").move_as_ok());
ASSERT_EQ(784887151, HttpDate::parse_http_date("Tue, 15 Nov 1994 08:12:31 GMT").move_as_ok());
}
TEST(Mtproto, config) {