// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021 // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // #include "td/telegram/LinkManager.h" #include "td/telegram/AccessRights.h" #include "td/telegram/ChannelId.h" #include "td/telegram/ConfigManager.h" #include "td/telegram/ConfigShared.h" #include "td/telegram/ContactsManager.h" #include "td/telegram/DialogId.h" #include "td/telegram/Global.h" #include "td/telegram/MessageEntity.h" #include "td/telegram/MessageId.h" #include "td/telegram/MessagesManager.h" #include "td/telegram/misc.h" #include "td/telegram/ServerMessageId.h" #include "td/telegram/Td.h" #include "td/telegram/TdDb.h" #include "td/telegram/telegram_api.h" #include "td/telegram/UserId.h" #include "td/mtproto/ProxySecret.h" #include "td/utils/algorithm.h" #include "td/utils/base64.h" #include "td/utils/buffer.h" #include "td/utils/HttpUrl.h" #include "td/utils/logging.h" #include "td/utils/misc.h" #include "td/utils/SliceBuilder.h" #include "td/utils/StringBuilder.h" #include "td/utils/Time.h" namespace td { static bool is_valid_start_parameter(Slice start_parameter) { return start_parameter.size() <= 64 && is_base64url_characters(start_parameter); } static bool is_valid_username(Slice username) { if (username.empty() || username.size() > 32) { return false; } if (!is_alpha(username[0])) { return false; } for (auto c : username) { if (!is_alpha(c) && !is_digit(c) && c != '_') { return false; } } if (username.back() == '_') { return false; } for (size_t i = 1; i < username.size(); i++) { if (username[i - 1] == '_' && username[i] == '_') { return false; } } return true; } static string get_url_query_hash(bool is_tg, const HttpUrlQuery &url_query) { const auto &path = url_query.path_; if (is_tg) { if (path.size() == 1 && path[0] == "join" && !url_query.get_arg("invite").empty()) { // join?invite=abcdef return url_query.get_arg("invite").str(); } } else { if (path.size() >= 2 && path[0] == "joinchat" && !path[1].empty()) { // /joinchat/ return path[1]; } if (!path.empty() && path[0].size() >= 2 && (path[0][0] == ' ' || path[0][0] == '+')) { // /+ return path[0].substr(1); } } return string(); } class LinkManager::InternalLinkActiveSessions final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkAuthenticationCode final : public InternalLink { string code_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(code_); } public: explicit InternalLinkAuthenticationCode(string code) : code_(std::move(code)) { } }; class LinkManager::InternalLinkBackground final : public InternalLink { string background_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(background_name_); } public: explicit InternalLinkBackground(string background_name) : background_name_(std::move(background_name)) { } }; class LinkManager::InternalLinkBotStart final : public InternalLink { string bot_username_; string start_parameter_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_username_, start_parameter_); } public: InternalLinkBotStart(string bot_username, string start_parameter) : bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) { } }; class LinkManager::InternalLinkBotStartInGroup final : public InternalLink { string bot_username_; string start_parameter_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_username_, start_parameter_); } public: InternalLinkBotStartInGroup(string bot_username, string start_parameter) : bot_username_(std::move(bot_username)), start_parameter_(std::move(start_parameter)) { } }; class LinkManager::InternalLinkChangePhoneNumber final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkConfirmPhone final : public InternalLink { string hash_; string phone_number_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(hash_, phone_number_); } public: InternalLinkConfirmPhone(string hash, string phone_number) : hash_(std::move(hash)), phone_number_(std::move(phone_number)) { } }; class LinkManager::InternalLinkDialogInvite final : public InternalLink { string url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(url_); } public: explicit InternalLinkDialogInvite(string url) : url_(std::move(url)) { } }; class LinkManager::InternalLinkFilterSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkGame final : public InternalLink { string bot_username_; string game_short_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_username_, game_short_name_); } public: InternalLinkGame(string bot_username, string game_short_name) : bot_username_(std::move(bot_username)), game_short_name_(std::move(game_short_name)) { } }; class LinkManager::InternalLinkLanguage final : public InternalLink { string language_pack_id_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(language_pack_id_); } public: explicit InternalLinkLanguage(string language_pack_id) : language_pack_id_(std::move(language_pack_id)) { } }; class LinkManager::InternalLinkMessage final : public InternalLink { string url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(url_); } public: explicit InternalLinkMessage(string url) : url_(std::move(url)) { } }; class LinkManager::InternalLinkMessageDraft final : public InternalLink { FormattedText text_; bool contains_link_ = false; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(get_formatted_text_object(text_, true, -1), contains_link_); } public: InternalLinkMessageDraft(FormattedText &&text, bool contains_link) : text_(std::move(text)), contains_link_(contains_link) { } }; class LinkManager::InternalLinkPassportDataRequest final : public InternalLink { UserId bot_user_id_; string scope_; string public_key_; string nonce_; string callback_url_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(bot_user_id_.get(), scope_, public_key_, nonce_, callback_url_); } public: InternalLinkPassportDataRequest(UserId bot_user_id, string scope, string public_key, string nonce, string callback_url) : bot_user_id_(bot_user_id) , scope_(std::move(scope)) , public_key_(std::move(public_key)) , nonce_(std::move(nonce)) , callback_url_(std::move(callback_url)) { } }; class LinkManager::InternalLinkProxy final : public InternalLink { string server_; int32 port_; td_api::object_ptr type_; td_api::object_ptr get_internal_link_type_object() const final { CHECK(type_ != nullptr); auto proxy_type = [type = type_.get()]() -> td_api::object_ptr { switch (type->get_id()) { case td_api::proxyTypeSocks5::ID: { auto type_socks = static_cast(type); return td_api::make_object(type_socks->username_, type_socks->password_); } case td_api::proxyTypeMtproto::ID: { auto type_mtproto = static_cast(type); return td_api::make_object(type_mtproto->secret_); } default: UNREACHABLE(); return nullptr; } }(); return td_api::make_object(server_, port_, std::move(proxy_type)); } public: InternalLinkProxy(string server, int32 port, td_api::object_ptr type) : server_(std::move(server)), port_(port), type_(std::move(type)) { } }; class LinkManager::InternalLinkPublicDialog final : public InternalLink { string dialog_username_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(dialog_username_); } public: explicit InternalLinkPublicDialog(string dialog_username) : dialog_username_(std::move(dialog_username)) { } }; class LinkManager::InternalLinkQrCodeAuthentication final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkStickerSet final : public InternalLink { string sticker_set_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(sticker_set_name_); } public: explicit InternalLinkStickerSet(string sticker_set_name) : sticker_set_name_(std::move(sticker_set_name)) { } }; class LinkManager::InternalLinkTheme final : public InternalLink { string theme_name_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(theme_name_); } public: explicit InternalLinkTheme(string theme_name) : theme_name_(std::move(theme_name)) { } }; class LinkManager::InternalLinkThemeSettings final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkUnknownDeepLink final : public InternalLink { string link_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(link_); } public: explicit InternalLinkUnknownDeepLink(string link) : link_(std::move(link)) { } }; class LinkManager::InternalLinkUnsupportedProxy final : public InternalLink { td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(); } }; class LinkManager::InternalLinkVoiceChat final : public InternalLink { string dialog_username_; string invite_hash_; bool is_live_stream_; td_api::object_ptr get_internal_link_type_object() const final { return td_api::make_object(dialog_username_, invite_hash_, is_live_stream_); } public: InternalLinkVoiceChat(string dialog_username, string invite_hash, bool is_live_stream) : dialog_username_(std::move(dialog_username)) , invite_hash_(std::move(invite_hash)) , is_live_stream_(is_live_stream) { } }; class GetDeepLinkInfoQuery final : public Td::ResultHandler { Promise> promise_; public: explicit GetDeepLinkInfoQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(Slice link) { send_query(G()->net_query_creator().create_unauth(telegram_api::help_getDeepLinkInfo(link.str()))); } void on_result(uint64 id, BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(id, result_ptr.move_as_error()); } auto result = result_ptr.move_as_ok(); switch (result->get_id()) { case telegram_api::help_deepLinkInfoEmpty::ID: return promise_.set_value(nullptr); case telegram_api::help_deepLinkInfo::ID: { auto info = telegram_api::move_object_as(result); auto entities = get_message_entities(nullptr, std::move(info->entities_), "GetDeepLinkInfoQuery"); auto status = fix_formatted_text(info->message_, entities, true, true, true, true, true); if (status.is_error()) { LOG(ERROR) << "Receive error " << status << " while parsing deep link info " << info->message_; if (!clean_input_string(info->message_)) { info->message_.clear(); } entities = find_entities(info->message_, true, true); } FormattedText text{std::move(info->message_), std::move(entities)}; return promise_.set_value( td_api::make_object(get_formatted_text_object(text, true, -1), info->update_app_)); } default: UNREACHABLE(); } } void on_error(uint64 id, Status status) final { promise_.set_error(std::move(status)); } }; class RequestUrlAuthQuery final : public Td::ResultHandler { Promise> promise_; string url_; DialogId dialog_id_; public: explicit RequestUrlAuthQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(string url, FullMessageId full_message_id, int32 button_id) { url_ = std::move(url); int32 flags = 0; tl_object_ptr input_peer; if (full_message_id.get_dialog_id().is_valid()) { dialog_id_ = full_message_id.get_dialog_id(); input_peer = td->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read); CHECK(input_peer != nullptr); flags |= telegram_api::messages_requestUrlAuth::PEER_MASK; } else { flags |= telegram_api::messages_requestUrlAuth::URL_MASK; } send_query(G()->net_query_creator().create(telegram_api::messages_requestUrlAuth( flags, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(), button_id, url_))); } void on_result(uint64 id, BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(id, result_ptr.move_as_error()); } auto result = result_ptr.move_as_ok(); LOG(INFO) << "Receive " << to_string(result); switch (result->get_id()) { case telegram_api::urlAuthResultRequest::ID: { auto request = telegram_api::move_object_as(result); UserId bot_user_id = ContactsManager::get_user_id(request->bot_); if (!bot_user_id.is_valid()) { return on_error(id, Status::Error(500, "Receive invalid bot_user_id")); } td->contacts_manager_->on_get_user(std::move(request->bot_), "RequestUrlAuthQuery"); promise_.set_value(td_api::make_object( url_, request->domain_, td->contacts_manager_->get_user_id_object(bot_user_id, "RequestUrlAuthQuery"), request->request_write_access_)); break; } case telegram_api::urlAuthResultAccepted::ID: { auto accepted = telegram_api::move_object_as(result); promise_.set_value(td_api::make_object(accepted->url_, true)); break; } case telegram_api::urlAuthResultDefault::ID: promise_.set_value(td_api::make_object(url_, false)); break; } } void on_error(uint64 id, Status status) final { if (!dialog_id_.is_valid() || !td->messages_manager_->on_get_dialog_error(dialog_id_, status, "RequestUrlAuthQuery")) { LOG(INFO) << "RequestUrlAuthQuery returned " << status; } promise_.set_value(td_api::make_object(url_, false)); } }; class AcceptUrlAuthQuery final : public Td::ResultHandler { Promise> promise_; string url_; DialogId dialog_id_; public: explicit AcceptUrlAuthQuery(Promise> &&promise) : promise_(std::move(promise)) { } void send(string url, FullMessageId full_message_id, int32 button_id, bool allow_write_access) { url_ = std::move(url); int32 flags = 0; tl_object_ptr input_peer; if (full_message_id.get_dialog_id().is_valid()) { dialog_id_ = full_message_id.get_dialog_id(); input_peer = td->messages_manager_->get_input_peer(dialog_id_, AccessRights::Read); CHECK(input_peer != nullptr); flags |= telegram_api::messages_acceptUrlAuth::PEER_MASK; } else { flags |= telegram_api::messages_acceptUrlAuth::URL_MASK; } if (allow_write_access) { flags |= telegram_api::messages_acceptUrlAuth::WRITE_ALLOWED_MASK; } send_query(G()->net_query_creator().create(telegram_api::messages_acceptUrlAuth( flags, false /*ignored*/, std::move(input_peer), full_message_id.get_message_id().get_server_message_id().get(), button_id, url_))); } void on_result(uint64 id, BufferSlice packet) final { auto result_ptr = fetch_result(packet); if (result_ptr.is_error()) { return on_error(id, result_ptr.move_as_error()); } auto result = result_ptr.move_as_ok(); LOG(INFO) << "Receive " << to_string(result); switch (result->get_id()) { case telegram_api::urlAuthResultRequest::ID: LOG(ERROR) << "Receive unexpected " << to_string(result); return on_error(id, Status::Error(500, "Receive unexpected urlAuthResultRequest")); case telegram_api::urlAuthResultAccepted::ID: { auto accepted = telegram_api::move_object_as(result); promise_.set_value(td_api::make_object(accepted->url_)); break; } case telegram_api::urlAuthResultDefault::ID: promise_.set_value(td_api::make_object(url_)); break; } } void on_error(uint64 id, Status status) final { if (!dialog_id_.is_valid() || !td->messages_manager_->on_get_dialog_error(dialog_id_, status, "AcceptUrlAuthQuery")) { LOG(INFO) << "AcceptUrlAuthQuery returned " << status; } promise_.set_error(std::move(status)); } }; LinkManager::LinkManager(Td *td, ActorShared<> parent) : td_(td), parent_(std::move(parent)) { } LinkManager::~LinkManager() = default; void LinkManager::start_up() { autologin_update_time_ = Time::now() - 365 * 86400; autologin_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("autologin_domains"), '\xFF'); url_auth_domains_ = full_split(G()->td_db()->get_binlog_pmc()->get("url_auth_domains"), '\xFF'); } void LinkManager::tear_down() { parent_.reset(); } static bool tolower_begins_with(Slice str, Slice prefix) { if (prefix.size() > str.size()) { return false; } for (size_t i = 0; i < prefix.size(); i++) { if (to_lower(str[i]) != prefix[i]) { return false; } } return true; } Result LinkManager::check_link(Slice link) { bool is_tg = false; bool is_ton = false; if (tolower_begins_with(link, "tg:")) { link.remove_prefix(3); is_tg = true; } else if (tolower_begins_with(link, "ton:")) { link.remove_prefix(4); is_ton = true; } if ((is_tg || is_ton) && begins_with(link, "//")) { link.remove_prefix(2); } TRY_RESULT(http_url, parse_url(link)); if (is_tg || is_ton) { if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https || !http_url.userinfo_.empty() || http_url.specified_port_ != 0 || http_url.is_ipv6_) { return Status::Error(is_tg ? Slice("Wrong tg URL") : Slice("Wrong ton URL")); } Slice query(http_url.query_); CHECK(query[0] == '/'); if (query.size() > 1 && query[1] == '?') { query.remove_prefix(1); } for (auto c : http_url.host_) { if (!is_alnum(c) && c != '-' && c != '_') { return Status::Error("Unallowed characters in URL host"); } } return PSTRING() << (is_tg ? "tg" : "ton") << "://" << http_url.host_ << query; } if (http_url.host_.find('.') == string::npos && !http_url.is_ipv6_) { return Status::Error("Wrong HTTP URL"); } return http_url.get_url(); } LinkManager::LinkInfo LinkManager::get_link_info(Slice link) { LinkInfo result; if (link.empty()) { return result; } link.truncate(link.find('#')); bool is_tg = false; if (tolower_begins_with(link, "tg:")) { link.remove_prefix(3); if (begins_with(link, "//")) { link.remove_prefix(2); } is_tg = true; } auto r_http_url = parse_url(link); if (r_http_url.is_error()) { return result; } auto http_url = r_http_url.move_as_ok(); if (!http_url.userinfo_.empty() || http_url.is_ipv6_) { return result; } if (is_tg) { if (tolower_begins_with(link, "http://") || http_url.protocol_ == HttpUrl::Protocol::Https || http_url.specified_port_ != 0) { return result; } result.is_internal_ = true; result.is_tg_ = true; result.query_ = link.str(); return result; } else { if (http_url.port_ != 80 && http_url.port_ != 443) { return result; } vector t_me_urls{Slice("t.me"), Slice("telegram.me"), Slice("telegram.dog")}; if (Scheduler::context() != nullptr) { // for tests only string cur_t_me_url = G()->shared_config().get_option_string("t_me_url"); if (tolower_begins_with(cur_t_me_url, "http://") || tolower_begins_with(cur_t_me_url, "https://")) { Slice t_me_url = cur_t_me_url; t_me_url = t_me_url.substr(t_me_url[4] == 's' ? 8 : 7); if (!td::contains(t_me_urls, t_me_url)) { t_me_urls.push_back(t_me_url); } } } auto host = url_decode(http_url.host_, false); to_lower_inplace(host); if (begins_with(host, "www.")) { host = host.substr(4); } for (auto t_me_url : t_me_urls) { if (host == t_me_url) { result.is_internal_ = true; result.is_tg_ = false; Slice query = http_url.query_; while (true) { if (begins_with(query, "/s/")) { query.remove_prefix(2); continue; } if (begins_with(query, "/%73/")) { query.remove_prefix(4); continue; } break; } result.query_ = query.str(); return result; } } } return result; } unique_ptr LinkManager::parse_internal_link(Slice link) { auto info = get_link_info(link); if (!info.is_internal_) { return nullptr; } if (info.is_tg_) { return parse_tg_link_query(info.query_); } else { return parse_t_me_link_query(info.query_); } } namespace { struct CopyArg { Slice name_; const HttpUrlQuery *url_query_; bool *is_first_; CopyArg(Slice name, const HttpUrlQuery *url_query, bool *is_first) : name_(name), url_query_(url_query), is_first_(is_first) { } }; StringBuilder &operator<<(StringBuilder &string_builder, const CopyArg ©_arg) { auto arg = copy_arg.url_query_->get_arg(copy_arg.name_); if (arg.empty()) { for (const auto &query_arg : copy_arg.url_query_->args_) { if (query_arg.first == copy_arg.name_) { char c = *copy_arg.is_first_ ? '?' : '&'; *copy_arg.is_first_ = false; return string_builder << c << copy_arg.name_; } } return string_builder; } char c = *copy_arg.is_first_ ? '?' : '&'; *copy_arg.is_first_ = false; return string_builder << c << copy_arg.name_ << '=' << url_encode(arg); } } // namespace unique_ptr LinkManager::parse_tg_link_query(Slice query) { const auto url_query = parse_url_query(query); const auto &path = url_query.path_; bool is_first_arg = true; auto copy_arg = [&](Slice name) { return CopyArg(name, &url_query, &is_first_arg); }; auto pass_arg = [&](Slice name) { return url_encode(url_query.get_arg(name)); }; auto get_arg = [&](Slice name) { return url_query.get_arg(name).str(); }; auto has_arg = [&](Slice name) { return !url_query.get_arg(name).empty(); }; if (path.size() == 1 && path[0] == "resolve") { if (is_valid_username(get_arg("domain"))) { if (has_arg("post")) { // resolve?domain=&post=12345&single&thread=&comment=&t= return td::make_unique(PSTRING() << "tg:resolve" << copy_arg("domain") << copy_arg("post") << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } auto username = get_arg("domain"); for (auto &arg : url_query.args_) { if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") { // resolve?domain=&videochat // resolve?domain=&videochat= if (Scheduler::context() != nullptr) { send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username); } return td::make_unique(std::move(username), arg.second, arg.first == "livestream"); } if (arg.first == "start" && is_valid_start_parameter(arg.second)) { // resolve?domain=?start= return td::make_unique(std::move(username), arg.second); } if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) { // resolve?domain=?startgroup= return td::make_unique(std::move(username), arg.second); } if (arg.first == "game" && !arg.second.empty()) { // resolve?domain=?game= return td::make_unique(std::move(username), arg.second); } } if (username == "telegrampassport") { // resolve?domain=telegrampassport&bot_id=&scope=&public_key=&nonce= return get_internal_link_passport(query, url_query.args_); } // resolve?domain= return td::make_unique(std::move(username)); } } else if (path.size() == 1 && path[0] == "login") { // login?code=123456 if (has_arg("code")) { return td::make_unique(get_arg("code")); } // login?token= if (has_arg("token")) { return td::make_unique(); } } else if (path.size() == 1 && path[0] == "passport") { // passport?bot_id=&scope=&public_key=&nonce= return get_internal_link_passport(query, url_query.args_); } else if (!path.empty() && path[0] == "settings") { if (path.size() == 2 && path[1] == "change_number") { // settings/change_number return td::make_unique(); } if (path.size() == 2 && path[1] == "devices") { // settings/devices return td::make_unique(); } if (path.size() == 2 && path[1] == "folders") { // settings/folders return td::make_unique(); } if (path.size() == 2 && path[1] == "themes") { // settings/themes return td::make_unique(); } // settings return td::make_unique(); } else if (path.size() == 1 && path[0] == "join") { // join?invite= if (has_arg("invite")) { return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(get_url_query_hash(true, url_query))); } } else if (path.size() == 1 && path[0] == "addstickers") { // addstickers?set= if (has_arg("set")) { return td::make_unique(get_arg("set")); } } else if (path.size() == 1 && path[0] == "setlanguage") { // setlanguage?lang= if (has_arg("lang")) { return td::make_unique(get_arg("lang")); } } else if (path.size() == 1 && path[0] == "addtheme") { // addtheme?slug= if (has_arg("slug")) { return td::make_unique(get_arg("slug")); } } else if (path.size() == 1 && path[0] == "confirmphone") { if (has_arg("hash") && has_arg("phone")) { // confirmphone?phone=&hash= return td::make_unique(get_arg("hash"), get_arg("phone")); } } else if (path.size() == 1 && path[0] == "socks") { if (has_arg("server") && has_arg("port")) { // socks?server=&port=&user=&pass= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536) { return td::make_unique( get_arg("server"), port, td_api::make_object(get_arg("user"), get_arg("pass"))); } else { return td::make_unique(); } } } else if (path.size() == 1 && path[0] == "proxy") { if (has_arg("server") && has_arg("port")) { // proxy?server=&port=&secret= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) { return td::make_unique(get_arg("server"), port, td_api::make_object(get_arg("secret"))); } else { return td::make_unique(); } } } else if (path.size() == 1 && path[0] == "privatepost") { // privatepost?channel=123456789&msg_id=12345&single&thread=&comment=&t= if (has_arg("channel") && has_arg("msg_id")) { return td::make_unique( PSTRING() << "tg:privatepost" << copy_arg("channel") << copy_arg("msg_id") << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } } else if (path.size() == 1 && path[0] == "bg") { // bg?color= // bg?gradient=-&rotation=... // bg?gradient=~~~ // bg?slug=&mode=blur+motion // bg?slug=&intensity=...&bg_color=...&mode=blur+motion if (has_arg("color")) { return td::make_unique(pass_arg("color")); } if (has_arg("gradient")) { return td::make_unique(PSTRING() << pass_arg("gradient") << copy_arg("rotation")); } if (has_arg("slug")) { return td::make_unique(PSTRING() << pass_arg("slug") << copy_arg("mode") << copy_arg("intensity") << copy_arg("bg_color") << copy_arg("rotation")); } } else if (path.size() == 1 && (path[0] == "share" || path[0] == "msg" || path[0] == "msg_url")) { // msg_url?url= // msg_url?url=&text= return get_internal_link_message_draft(get_arg("url"), get_arg("text")); } if (!path.empty() && !path[0].empty()) { return td::make_unique(PSTRING() << "tg://" << query); } return nullptr; } unique_ptr LinkManager::parse_t_me_link_query(Slice query) { CHECK(query[0] == '/'); const auto url_query = parse_url_query(query); const auto &path = url_query.path_; if (path.empty() || path[0].empty()) { return nullptr; } bool is_first_arg = true; auto copy_arg = [&](Slice name) { return CopyArg(name, &url_query, &is_first_arg); }; auto get_arg = [&](Slice name) { return url_query.get_arg(name).str(); }; auto has_arg = [&](Slice name) { return !url_query.get_arg(name).empty(); }; if (path[0] == "c") { if (path.size() >= 3 && to_integer(path[1]) > 0 && to_integer(path[2]) > 0) { // /c/123456789/12345?single&thread=&comment=&t= is_first_arg = false; return td::make_unique(PSTRING() << "tg:privatepost?channel=" << to_integer(path[1]) << "&msg_id=" << to_integer(path[2]) << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } } else if (path[0] == "login") { if (path.size() >= 2 && !path[1].empty()) { // /login/ return td::make_unique(path[1]); } } else if (path[0] == "joinchat") { if (path.size() >= 2 && !path[1].empty()) { // /joinchat/ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(get_url_query_hash(false, url_query))); } } else if (path[0][0] == ' ' || path[0][0] == '+') { if (path[0].size() >= 2) { // /+ return td::make_unique(PSTRING() << "tg:join?invite=" << url_encode(get_url_query_hash(false, url_query))); } } else if (path[0] == "addstickers") { if (path.size() >= 2 && !path[1].empty()) { // /addstickers/ return td::make_unique(path[1]); } } else if (path[0] == "setlanguage") { if (path.size() >= 2 && !path[1].empty()) { // /setlanguage/ return td::make_unique(path[1]); } } else if (path[0] == "addtheme") { if (path.size() >= 2 && !path[1].empty()) { // /addtheme/ return td::make_unique(path[1]); } } else if (path[0] == "confirmphone") { if (has_arg("hash") && has_arg("phone")) { // /confirmphone?phone=&hash= return td::make_unique(get_arg("hash"), get_arg("phone")); } } else if (path[0] == "socks") { if (has_arg("server") && has_arg("port")) { // /socks?server=&port=&user=&pass= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536) { return td::make_unique( get_arg("server"), port, td_api::make_object(get_arg("user"), get_arg("pass"))); } else { return td::make_unique(); } } } else if (path[0] == "proxy") { if (has_arg("server") && has_arg("port")) { // /proxy?server=&port=&secret= auto port = to_integer(get_arg("port")); if (0 < port && port < 65536 && mtproto::ProxySecret::from_link(get_arg("secret")).is_ok()) { return td::make_unique(get_arg("server"), port, td_api::make_object(get_arg("secret"))); } else { return td::make_unique(); } } } else if (path[0] == "bg") { if (path.size() >= 2 && !path[1].empty()) { // /bg/ // /bg/-?rotation=... // /bg/~~~ // /bg/?mode=blur+motion // /bg/?intensity=...&bg_color=...&mode=blur+motion return td::make_unique(PSTRING() << url_encode(path[1]) << copy_arg("mode") << copy_arg("intensity") << copy_arg("bg_color") << copy_arg("rotation")); } } else if (path[0] == "share" || path[0] == "msg") { if (!(path.size() > 1 && (path[1] == "bookmarklet" || path[1] == "embed"))) { // /share?url= // /share/url?url=&text= return get_internal_link_message_draft(get_arg("url"), get_arg("text")); } } else if (is_valid_username(path[0])) { if (path.size() >= 2 && to_integer(path[1]) > 0) { // //12345?single&thread=&comment=&t= is_first_arg = false; return td::make_unique( PSTRING() << "tg:resolve?domain=" << url_encode(path[0]) << "&post=" << to_integer(path[1]) << copy_arg("single") << copy_arg("thread") << copy_arg("comment") << copy_arg("t")); } auto username = path[0]; for (auto &arg : url_query.args_) { if (arg.first == "voicechat" || arg.first == "videochat" || arg.first == "livestream") { // /?videochat // /?videochat= if (Scheduler::context() != nullptr) { send_closure(G()->messages_manager(), &MessagesManager::reload_voice_chat_on_search, username); } return td::make_unique(std::move(username), arg.second, arg.first == "livestream"); } if (arg.first == "start" && is_valid_start_parameter(arg.second)) { // /?start= return td::make_unique(std::move(username), arg.second); } if (arg.first == "startgroup" && is_valid_start_parameter(arg.second)) { // /?startgroup= return td::make_unique(std::move(username), arg.second); } if (arg.first == "game" && !arg.second.empty()) { // /?game= return td::make_unique(std::move(username), arg.second); } } // / return td::make_unique(std::move(username)); } return nullptr; } unique_ptr LinkManager::get_internal_link_message_draft(Slice url, Slice text) { if (url.empty() && text.empty()) { return nullptr; } while (!text.empty() && text.back() == '\n') { text.remove_suffix(1); } url = trim(url); if (url.empty()) { url = text; text = Slice(); } FormattedText full_text; bool contains_url = false; if (!text.empty()) { contains_url = true; full_text.text = PSTRING() << url << '\n' << text; } else { full_text.text = url.str(); } if (fix_formatted_text(full_text.text, full_text.entities, false, false, false, true, true).is_error()) { return nullptr; } if (full_text.text[0] == '@') { full_text.text = ' ' + full_text.text; for (auto &entity : full_text.entities) { entity.offset++; } } return td::make_unique(std::move(full_text), contains_url); } unique_ptr LinkManager::get_internal_link_passport( Slice query, const vector> &args) { auto get_arg = [&args](Slice key) { for (auto &arg : args) { if (arg.first == key) { return Slice(arg.second); } } return Slice(); }; UserId bot_user_id(to_integer(get_arg("bot_id"))); auto scope = get_arg("scope"); auto public_key = get_arg("public_key"); auto nonce = get_arg("nonce"); if (nonce.empty()) { nonce = get_arg("payload"); } auto callback_url = get_arg("callback_url"); if (!bot_user_id.is_valid() || scope.empty() || public_key.empty() || nonce.empty()) { return td::make_unique(PSTRING() << "tg://" << query); } return td::make_unique(bot_user_id, scope.str(), public_key.str(), nonce.str(), callback_url.str()); } void LinkManager::update_autologin_domains(string autologin_token, vector autologin_domains, vector url_auth_domains) { autologin_update_time_ = Time::now(); autologin_token_ = std::move(autologin_token); if (autologin_domains_ != autologin_domains) { autologin_domains_ = std::move(autologin_domains); G()->td_db()->get_binlog_pmc()->set("autologin_domains", implode(autologin_domains_, '\xFF')); } if (url_auth_domains_ != url_auth_domains) { url_auth_domains_ = std::move(url_auth_domains); G()->td_db()->get_binlog_pmc()->set("url_auth_domains", implode(url_auth_domains_, '\xFF')); } } void LinkManager::get_deep_link_info(Slice link, Promise> &&promise) { Slice link_scheme("tg:"); if (begins_with(link, link_scheme)) { link.remove_prefix(link_scheme.size()); if (begins_with(link, "//")) { link.remove_prefix(2); } } size_t pos = 0; while (pos < link.size() && link[pos] != '/' && link[pos] != '?' && link[pos] != '#') { pos++; } link.truncate(pos); td_->create_handler(std::move(promise))->send(link); } void LinkManager::get_external_link_info(string &&link, Promise> &&promise) { auto default_result = td_api::make_object(link, false); if (G()->close_flag()) { return promise.set_value(std::move(default_result)); } auto r_url = parse_url(link); if (r_url.is_error()) { return promise.set_value(std::move(default_result)); } if (!td::contains(autologin_domains_, r_url.ok().host_)) { if (td::contains(url_auth_domains_, r_url.ok().host_)) { td_->create_handler(std::move(promise))->send(link, FullMessageId(), 0); return; } return promise.set_value(std::move(default_result)); } if (autologin_update_time_ < Time::now() - 10000) { auto query_promise = PromiseCreator::lambda([link = std::move(link), promise = std::move(promise)](Result &&result) mutable { if (result.is_error()) { return promise.set_value(td_api::make_object(link, false)); } send_closure(G()->link_manager(), &LinkManager::get_external_link_info, std::move(link), std::move(promise)); }); return send_closure(G()->config_manager(), &ConfigManager::reget_app_config, std::move(query_promise)); } if (autologin_token_.empty()) { return promise.set_value(std::move(default_result)); } auto url = r_url.move_as_ok(); url.protocol_ = HttpUrl::Protocol::Https; Slice path = url.query_; path.truncate(url.query_.find_first_of("?#")); Slice parameters_hash = Slice(url.query_).substr(path.size()); Slice parameters = parameters_hash; parameters.truncate(parameters.find('#')); Slice hash = parameters_hash.substr(parameters.size()); string added_parameter; if (parameters.empty()) { added_parameter = '?'; } else if (parameters.size() == 1) { CHECK(parameters == "?"); } else { added_parameter = '&'; } added_parameter += "autologin_token="; added_parameter += autologin_token_; url.query_ = PSTRING() << path << parameters << added_parameter << hash; promise.set_value(td_api::make_object(url.get_url(), false)); } void LinkManager::get_login_url_info(FullMessageId full_message_id, int64 button_id, Promise> &&promise) { TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id)); td_->create_handler(std::move(promise)) ->send(std::move(url), full_message_id, narrow_cast(button_id)); } void LinkManager::get_login_url(FullMessageId full_message_id, int64 button_id, bool allow_write_access, Promise> &&promise) { TRY_RESULT_PROMISE(promise, url, td_->messages_manager_->get_login_button_url(full_message_id, button_id)); td_->create_handler(std::move(promise)) ->send(std::move(url), full_message_id, narrow_cast(button_id), allow_write_access); } void LinkManager::get_link_login_url(const string &url, bool allow_write_access, Promise> &&promise) { td_->create_handler(std::move(promise))->send(url, FullMessageId(), 0, allow_write_access); } string LinkManager::get_dialog_invite_link_hash(Slice invite_link) { auto link_info = get_link_info(invite_link); if (!link_info.is_internal_) { return string(); } const auto url_query = parse_url_query(link_info.query_); return get_url_query_hash(link_info.is_tg_, url_query); } Result LinkManager::get_message_link_info(Slice url) { if (url.empty()) { return Status::Error("URL must be non-empty"); } auto link_info = get_link_info(url); if (!link_info.is_internal_) { return Status::Error("Invalid message link URL"); } url = link_info.query_; Slice username; Slice channel_id_slice; Slice message_id_slice; Slice comment_message_id_slice = "0"; Slice media_timestamp_slice; bool is_single = false; bool for_comment = false; if (link_info.is_tg_) { // resolve?domain=username&post=12345&single&t=123&comment=12&thread=21 // privatepost?channel=123456789&msg_id=12345&single&t=123&comment=12&thread=21 bool is_resolve = false; if (begins_with(url, "resolve")) { url = url.substr(7); is_resolve = true; } else if (begins_with(url, "privatepost")) { url = url.substr(11); } else { return Status::Error("Wrong message link URL"); } if (begins_with(url, "/")) { url = url.substr(1); } if (!begins_with(url, "?")) { return Status::Error("Wrong message link URL"); } url = url.substr(1); auto args = full_split(url, '&'); for (auto arg : args) { auto key_value = split(arg, '='); if (is_resolve) { if (key_value.first == "domain") { username = key_value.second; } if (key_value.first == "post") { message_id_slice = key_value.second; } } else { if (key_value.first == "channel") { channel_id_slice = key_value.second; } if (key_value.first == "msg_id") { message_id_slice = key_value.second; } } if (key_value.first == "t") { media_timestamp_slice = key_value.second; } if (key_value.first == "single") { is_single = true; } if (key_value.first == "comment") { comment_message_id_slice = key_value.second; } if (key_value.first == "thread") { for_comment = true; } } } else { // /c/123456789/12345 // /username/12345?single CHECK(!url.empty() && url[0] == '/'); url.remove_prefix(1); auto username_end_pos = url.find('/'); if (username_end_pos == Slice::npos) { return Status::Error("Wrong message link URL"); } username = url.substr(0, username_end_pos); url = url.substr(username_end_pos + 1); if (username == "c") { username = Slice(); auto channel_id_end_pos = url.find('/'); if (channel_id_end_pos == Slice::npos) { return Status::Error("Wrong message link URL"); } channel_id_slice = url.substr(0, channel_id_end_pos); url = url.substr(channel_id_end_pos + 1); } auto query_pos = url.find('?'); message_id_slice = url.substr(0, query_pos); if (query_pos != Slice::npos) { auto args = full_split(url.substr(query_pos + 1), '&'); for (auto arg : args) { auto key_value = split(arg, '='); if (key_value.first == "t") { media_timestamp_slice = key_value.second; } if (key_value.first == "single") { is_single = true; } if (key_value.first == "comment") { comment_message_id_slice = key_value.second; } if (key_value.first == "thread") { for_comment = true; } } } } ChannelId channel_id; if (username.empty()) { auto r_channel_id = to_integer_safe(channel_id_slice); if (r_channel_id.is_error() || !ChannelId(r_channel_id.ok()).is_valid()) { return Status::Error("Wrong channel ID"); } channel_id = ChannelId(r_channel_id.ok()); } auto r_message_id = to_integer_safe(message_id_slice); if (r_message_id.is_error() || !ServerMessageId(r_message_id.ok()).is_valid()) { return Status::Error("Wrong message ID"); } auto r_comment_message_id = to_integer_safe(comment_message_id_slice); if (r_comment_message_id.is_error() || !(r_comment_message_id.ok() == 0 || ServerMessageId(r_comment_message_id.ok()).is_valid())) { return Status::Error("Wrong comment message ID"); } bool is_media_timestamp_invalid = false; int32 media_timestamp = 0; const int32 MAX_MEDIA_TIMESTAMP = 10000000; if (!media_timestamp_slice.empty()) { int32 current_value = 0; for (size_t i = 0; i <= media_timestamp_slice.size(); i++) { auto c = i < media_timestamp_slice.size() ? media_timestamp_slice[i] : 's'; if ('0' <= c && c <= '9') { current_value = current_value * 10 + c - '0'; if (current_value > MAX_MEDIA_TIMESTAMP) { is_media_timestamp_invalid = true; break; } } else { auto mul = 0; switch (to_lower(c)) { case 'h': mul = 3600; break; case 'm': mul = 60; break; case 's': mul = 1; break; } if (mul == 0 || current_value > MAX_MEDIA_TIMESTAMP / mul || media_timestamp + current_value * mul > MAX_MEDIA_TIMESTAMP) { is_media_timestamp_invalid = true; break; } media_timestamp += current_value * mul; current_value = 0; } } } MessageLinkInfo info; info.username = username.str(); info.channel_id = channel_id; info.message_id = MessageId(ServerMessageId(r_message_id.ok())); info.comment_message_id = MessageId(ServerMessageId(r_comment_message_id.ok())); info.media_timestamp = is_media_timestamp_invalid ? 0 : media_timestamp; info.is_single = is_single; info.for_comment = for_comment; LOG(INFO) << "Have link to " << info.message_id << " in chat @" << info.username << "/" << channel_id.get(); return std::move(info); } } // namespace td