// This file Copyright © Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include #include #include // operator""ms #include // size_t #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LIBTRANSMISSION_ANNOUNCER_MODULE #include "libtransmission/transmission.h" #include "libtransmission/announce-list.h" #include "libtransmission/announcer-common.h" #include "libtransmission/announcer.h" #include "libtransmission/crypto-utils.h" /* tr_rand_int() */ #include "libtransmission/interned-string.h" // tr_interned_string #include "libtransmission/log.h" #include "libtransmission/session.h" #include "libtransmission/timer.h" #include "libtransmission/torrent.h" #include "libtransmission/tr-assert.h" #include "libtransmission/tr-macros.h" // tr_sha1_digest_t, TR_C... #include "libtransmission/utils.h" #include "libtransmission/web-utils.h" using namespace std::literals; #define tr_logAddErrorTier(tier, msg) tr_logAddError(msg, (tier)->buildLogName()) #define tr_logAddWarnTier(tier, msg) tr_logAddWarn(msg, (tier)->buildLogName()) #define tr_logAddDebugTier(tier, msg) tr_logAddDebug(msg, (tier)->buildLogName()) #define tr_logAddTraceTier(tier, msg) tr_logAddTrace(msg, (tier)->buildLogName()) namespace { /* unless the tracker says otherwise, rescrape this frequently */ auto constexpr DefaultScrapeIntervalSec = 60 * 30; /* the value of the 'numwant' argument passed in tracker requests. */ auto constexpr Numwant = 80; /* how often to announce & scrape */ auto constexpr MaxAnnouncesPerUpkeep = 20; auto constexpr MaxScrapesPerUpkeep = 20; /* how many infohashes to remove when we get a scrape-too-long error */ auto constexpr TrMultiscrapeStep = 5; struct StopsCompare { [[nodiscard]] static constexpr auto compare(tr_announce_request const& one, tr_announce_request const& two) noexcept // <=> { // primary key: volume of data transferred auto const ax = one.up + one.down; auto const bx = two.up + two.down; if (auto const val = tr_compare_3way(ax, bx); val != 0) { return val; } // secondary key: the torrent's info_hash for (size_t i = 0, n = sizeof(tr_sha1_digest_t); i < n; ++i) { if (auto const val = tr_compare_3way(one.info_hash[i], two.info_hash[i]); val != 0) { return val; } } // tertiary key: the tracker's announce url return tr_compare_3way(one.announce_url, two.announce_url); } [[nodiscard]] constexpr auto operator()(tr_announce_request const& one, tr_announce_request const& two) const noexcept { return compare(one, two) < 0; } }; } // namespace // --- struct tr_scrape_info { int multiscrape_max; tr_interned_string scrape_url; constexpr tr_scrape_info(tr_interned_string scrape_url_in, int const multiscrape_max_in) : multiscrape_max{ multiscrape_max_in } , scrape_url{ scrape_url_in } { } }; /** * "global" (per-tr_session) fields */ class tr_announcer_impl final : public tr_announcer { public: explicit tr_announcer_impl(tr_session* session_in, tr_announcer_udp& announcer_udp) : session{ session_in } , announcer_udp_{ announcer_udp } , upkeep_timer_{ session_in->timerMaker().create() } { upkeep_timer_->set_callback([this]() { this->upkeep(); }); upkeep_timer_->start_repeating(UpkeepInterval); } ~tr_announcer_impl() override { flushCloseMessages(); } tr_announcer_impl(tr_announcer_impl&&) = delete; tr_announcer_impl(tr_announcer_impl const&) = delete; tr_announcer_impl& operator=(tr_announcer_impl&&) = delete; tr_announcer_impl& operator=(tr_announcer_impl const&) = delete; tr_torrent_announcer* addTorrent(tr_torrent* tor, tr_tracker_callback callback) override; void startTorrent(tr_torrent* tor) override; void stopTorrent(tr_torrent* tor) override; void resetTorrent(tr_torrent* tor) override; void removeTorrent(tr_torrent* tor) override; void startShutdown() override { is_shutting_down_ = true; flushCloseMessages(); } void upkeep() override; void onAnnounceDone(int tier_id, tr_announce_event event, bool is_running_on_success, tr_announce_response const& response); void onScrapeDone(tr_scrape_response const& response); [[nodiscard]] tr_scrape_info* scrape_info(tr_interned_string url) { if (std::empty(url)) { return nullptr; } auto const [it, is_new] = scrape_info_.try_emplace(url, url, TR_MULTISCRAPE_MAX); return &it->second; } void scrape(tr_scrape_request const& request, tr_scrape_response_func on_response) { TR_ASSERT(!is_shutting_down_); if (auto const scrape_sv = request.scrape_url.sv(); tr_strv_starts_with(scrape_sv, "http://"sv) || tr_strv_starts_with(scrape_sv, "https://"sv)) { tr_tracker_http_scrape(session, request, std::move(on_response)); } else if (tr_strv_starts_with(scrape_sv, "udp://"sv)) { announcer_udp_.scrape(request, std::move(on_response)); } else { tr_logAddError(fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", scrape_sv))); } } void announce(tr_announce_request const& request, tr_announce_response_func on_response) { TR_ASSERT(!is_shutting_down_ || request.event == TR_ANNOUNCE_EVENT_STOPPED); if (auto const announce_sv = request.announce_url.sv(); tr_strv_starts_with(announce_sv, "http://"sv) || tr_strv_starts_with(announce_sv, "https://"sv)) { tr_tracker_http_announce(session, request, std::move(on_response)); } else if (tr_strv_starts_with(announce_sv, "udp://"sv)) { announcer_udp_.announce(request, std::move(on_response)); } else { tr_logAddWarn(fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", announce_sv))); } } tr_session* const session; private: void flushCloseMessages() { for (auto& stop : stops_) { announce(stop, [](tr_announce_response const& /*response*/) {}); } stops_.clear(); } static auto constexpr UpkeepInterval = 500ms; tr_announcer_udp& announcer_udp_; std::map scrape_info_; std::unique_ptr const upkeep_timer_; std::set stops_; bool is_shutting_down_ = false; }; std::unique_ptr tr_announcer::create(tr_session* session, tr_announcer_udp& announcer_udp) { TR_ASSERT(session != nullptr); return std::make_unique(session, announcer_udp); } // --- /* a row in tr_tier's list of trackers */ struct tr_tracker { explicit tr_tracker(tr_announcer_impl* announcer, tr_announce_list::tracker_info const& info) : announce_url{ info.announce } , announce_parsed{ info.announce_parsed } , scrape_info{ std::empty(info.scrape) ? nullptr : announcer->scrape_info(info.scrape) } , id{ info.id } { } [[nodiscard]] time_t getRetryInterval() const { switch (consecutive_failures) { case 0: return 0U; case 1: return 20U; case 2: return tr_rand_int(60U) + 60U * 5U; case 3: return tr_rand_int(60U) + 60U * 15U; case 4: return tr_rand_int(60U) + 60U * 30U; case 5: return tr_rand_int(60U) + 60U * 60U; default: return tr_rand_int(60U) + 60U * 120U; } } [[nodiscard]] constexpr auto seeder_count() const noexcept { return seeder_count_; } constexpr bool set_seeder_count(std::optional seeder_count_in) noexcept { if (seeder_count_in >= 0) { seeder_count_ = seeder_count_in; return true; } return false; } [[nodiscard]] constexpr auto leecher_count() const noexcept { return leecher_count_; } constexpr bool set_leecher_count(std::optional leecher_count_in) noexcept { if (leecher_count_in >= 0) { leecher_count_ = leecher_count_in; return true; } return false; } [[nodiscard]] constexpr auto download_count() const noexcept { return download_count_; } constexpr bool set_download_count(std::optional download_count_in) noexcept { if (download_count_in >= 0) { download_count_ = download_count_in; return true; } return false; } [[nodiscard]] constexpr auto downloader_count() const noexcept { return downloader_count_; } constexpr bool set_downloader_count(std::optional downloader_count_in) noexcept { if (downloader_count_in >= 0) { downloader_count_ = downloader_count_in; return true; } return false; } tr_interned_string const announce_url; tr_url_parsed_t const announce_parsed; tr_scrape_info* const scrape_info; std::string tracker_id; int consecutive_failures = 0; tr_tracker_id_t const id; private: std::optional seeder_count_; std::optional leecher_count_; std::optional download_count_; std::optional downloader_count_; }; // --- /** @brief A group of trackers in a single tier, as per the multitracker spec */ struct tr_tier { tr_tier(tr_announcer_impl* announcer, tr_torrent* tor_in, std::vector const& infos) : tor{ tor_in } { trackers.reserve(std::size(infos)); for (auto const* info : infos) { trackers.emplace_back(announcer, *info); } useNextTracker(); scrapeSoon(); } [[nodiscard]] tr_tracker* currentTracker() { if (!current_tracker_index_) { return nullptr; } TR_ASSERT(*current_tracker_index_ < std::size(trackers)); return &trackers[*current_tracker_index_]; } [[nodiscard]] tr_tracker const* currentTracker() const { if (!current_tracker_index_) { return nullptr; } TR_ASSERT(*current_tracker_index_ < std::size(trackers)); return &trackers[*current_tracker_index_]; } [[nodiscard]] constexpr bool needsToAnnounce(time_t now) const { return !isAnnouncing && !isScraping && announceAt != 0 && announceAt <= now && !std::empty(announce_events); } [[nodiscard]] bool needsToScrape(time_t now) const { auto const* const tracker = currentTracker(); return !isScraping && scrapeAt != 0 && scrapeAt <= now && tracker != nullptr && tracker->scrape_info != nullptr; } [[nodiscard]] auto countDownloaders() const { auto const* const tracker = currentTracker(); return tracker == nullptr ? 0 : tracker->downloader_count().value_or(-1) + tracker->leecher_count().value_or(-1); } tr_tracker* useNextTracker() { // move our index to the next tracker in the tier if (std::empty(trackers)) { current_tracker_index_ = std::nullopt; } else if (!current_tracker_index_) { current_tracker_index_ = 0; } else { current_tracker_index_ = (*current_tracker_index_ + 1) % std::size(trackers); } // reset some of the tier's fields scrapeIntervalSec = DefaultScrapeIntervalSec; announceIntervalSec = DefaultAnnounceIntervalSec; announceMinIntervalSec = DefaultAnnounceMinIntervalSec; isAnnouncing = false; isScraping = false; lastAnnounceStartTime = 0; lastScrapeStartTime = 0; return currentTracker(); } [[nodiscard]] std::optional indexOf(tr_interned_string announce_url) const { for (size_t i = 0, n = std::size(trackers); i < n; ++i) { if (announce_url == trackers[i].announce_url) { return i; } } return std::nullopt; } [[nodiscard]] auto buildLogName() const { auto const* const tracker = currentTracker(); return tracker != nullptr ? fmt::format("{:s} at {:s}:{:d}", tor->name(), tracker->announce_parsed.host, tracker->announce_parsed.port) : fmt::format("{:s} at ?", tor->name()); } [[nodiscard]] bool canManualAnnounce() const { return this->manualAnnounceAllowedAt <= tr_time(); } void scheduleNextScrape() { scheduleNextScrape(this->scrapeIntervalSec); } void scrapeSoon() { scheduleNextScrape(0); } void scheduleNextScrape(time_t interval_secs) { this->scrapeAt = getNextScrapeTime(tor->session, this, interval_secs); } std::deque announce_events; std::string last_announce_str; std::string last_scrape_str; /* number of up/down/corrupt bytes since the last time we sent an * "event=stopped" message that was acknowledged by the tracker */ std::array byteCounts = {}; std::vector trackers; std::optional current_tracker_index_; tr_torrent* const tor; time_t scrapeAt = 0; time_t lastScrapeStartTime = 0; time_t lastScrapeTime = 0; time_t announceAt = 0; time_t manualAnnounceAllowedAt = 0; time_t lastAnnounceStartTime = 0; time_t lastAnnounceTime = 0; int const id = next_key++; int announce_event_priority = 0; int scrapeIntervalSec = DefaultScrapeIntervalSec; int announceIntervalSec = DefaultAnnounceIntervalSec; int announceMinIntervalSec = DefaultAnnounceMinIntervalSec; size_t lastAnnouncePeerCount = 0; bool lastScrapeSucceeded = false; bool lastScrapeTimedOut = false; bool lastAnnounceSucceeded = false; bool lastAnnounceTimedOut = false; bool isRunning = false; bool isAnnouncing = false; bool isScraping = false; private: // unless the tracker says otherwise, this is the announce interval static auto constexpr DefaultAnnounceIntervalSec = 60 * 10; // unless the tracker says otherwise, this is the announce min_interval static auto constexpr DefaultAnnounceMinIntervalSec = 60 * 2; [[nodiscard]] static time_t getNextScrapeTime(tr_session const* session, tr_tier const* tier, time_t interval_secs) { // Maybe don't scrape paused torrents if (!tier->isRunning && !session->shouldScrapePausedTorrents()) { return 0; } /* Add the interval, and then increment to the nearest 10th second. * The latter step is to increase the odds of several torrents coming * due at the same time to improve multiscrape. */ auto ret = tr_time() + interval_secs; while (ret % 10U != 0U) { ++ret; } return ret; } static inline int next_key = 0; }; // --- /** * @brief Opaque, per-torrent data structure for tracker announce information * * this opaque data structure can be found in tr_torrent.tiers */ struct tr_torrent_announcer { tr_torrent_announcer(tr_announcer_impl* announcer, tr_torrent* tor) { // build the trackers auto tier_to_infos = std::map>{}; auto const announce_list = getAnnounceList(tor); for (auto const& info : announce_list) { tier_to_infos[info.tier].emplace_back(&info); } for (auto const& [tier_num, infos] : tier_to_infos) { tiers.emplace_back(announcer, tor, infos); } } tr_tier* getTier(int tier_id) { for (auto& tier : tiers) { if (tier.id == tier_id) { return &tier; } } return nullptr; } tr_tier* getTierFromScrape(tr_interned_string scrape_url) { for (auto& tier : tiers) { auto const* const tracker = tier.currentTracker(); if (tracker != nullptr && tracker->scrape_info != nullptr && tracker->scrape_info->scrape_url == scrape_url) { return &tier; } } return nullptr; } [[nodiscard]] bool canManualAnnounce() const { return std::any_of(std::begin(tiers), std::end(tiers), [](auto const& tier) { return tier.canManualAnnounce(); }); } [[nodiscard]] bool findTracker( tr_interned_string announce_url, tr_tier const** setme_tier, tr_tracker const** setme_tracker) const { for (auto const& tier : tiers) { for (auto const& tracker : tier.trackers) { if (tracker.announce_url == announce_url) { *setme_tier = &tier; *setme_tracker = &tracker; return true; } } } return false; } std::vector tiers; tr_tracker_callback callback; private: [[nodiscard]] static tr_announce_list getAnnounceList(tr_torrent const* tor) { auto announce_list = tor->announce_list(); // if it's a public torrent, add the default trackers if (tor->is_public()) { announce_list.add(tor->session->defaultTrackers()); } return announce_list; } }; // --- PUBLISH namespace { namespace publish_helpers { void publishMessage(tr_tier* tier, std::string_view msg, tr_tracker_event::Type type) { if (tier != nullptr && tier->tor != nullptr && tier->tor->torrent_announcer != nullptr && tier->tor->torrent_announcer->callback != nullptr) { auto* const ta = tier->tor->torrent_announcer; auto event = tr_tracker_event{}; event.type = type; event.text = msg; if (auto const* const current_tracker = tier->currentTracker(); current_tracker != nullptr) { event.announce_url = current_tracker->announce_url; } ta->callback(*tier->tor, &event); } } void publishErrorClear(tr_tier* tier) { publishMessage(tier, ""sv, tr_tracker_event::Type::ErrorClear); } void publishWarning(tr_tier* tier, std::string_view msg) { publishMessage(tier, msg, tr_tracker_event::Type::Warning); } void publishError(tr_tier* tier, std::string_view msg) { publishMessage(tier, msg, tr_tracker_event::Type::Error); } void publishPeerCounts(tr_tier* tier, std::optional seeders, std::optional leechers) { if (tier->tor->torrent_announcer->callback != nullptr) { auto e = tr_tracker_event{}; e.type = tr_tracker_event::Type::Counts; e.seeders = seeders; e.leechers = leechers; tr_logAddDebugTier( tier, fmt::format("peer counts: {} seeders, {} leechers.", seeders.value_or(-1), leechers.value_or(-1))); tier->tor->torrent_announcer->callback(*tier->tor, &e); } } void publishPeersPex(tr_tier* tier, std::vector const& pex) { if (tier->tor->torrent_announcer->callback != nullptr) { auto e = tr_tracker_event{}; e.type = tr_tracker_event::Type::Peers; e.pex = pex; tr_logAddDebugTier(tier, fmt::format("tracker gave a list of {} peers.", std::size(pex))); tier->tor->torrent_announcer->callback(*tier->tor, &e); } } } // namespace publish_helpers } // namespace // --- tr_torrent_announcer* tr_announcer_impl::addTorrent(tr_torrent* tor, tr_tracker_callback callback) { TR_ASSERT(tr_isTorrent(tor)); auto* ta = new tr_torrent_announcer{ this, tor }; ta->callback = std::move(callback); return ta; } // --- bool tr_announcerCanManualAnnounce(tr_torrent const* tor) { TR_ASSERT(tr_isTorrent(tor)); TR_ASSERT(tor->torrent_announcer != nullptr); return tor->is_running() && tor->torrent_announcer->canManualAnnounce(); } time_t tr_announcerNextManualAnnounce(tr_torrent const* tor) { time_t ret = ~(time_t)0; for (auto const& tier : tor->torrent_announcer->tiers) { if (tier.isRunning) { ret = std::min(ret, tier.manualAnnounceAllowedAt); } } return ret; } namespace { namespace announce_helpers { void tr_logAddTrace_tier_announce_queue(tr_tier const* tier) { if (!tr_logLevelIsActive(TR_LOG_TRACE) || std::empty(tier->announce_events)) { return; } auto buf = std::string{}; auto const& events = tier->announce_events; buf.reserve(std::size(events) * 20); for (size_t i = 0, n = std::size(events); i < n; ++i) { fmt::format_to(std::back_inserter(buf), "[{:d}:{:s}]", i, tr_announce_event_get_string(events[i])); } tr_logAddTraceTier(tier, std::move(buf)); } // higher priorities go to the front of the announce queue void tier_update_announce_priority(tr_tier* tier) { int priority = -1; for (auto const& event : tier->announce_events) { priority = std::max(priority, int{ event }); } tier->announce_event_priority = priority; } void tier_announce_remove_trailing(tr_tier* tier, tr_announce_event e) { while (!std::empty(tier->announce_events) && tier->announce_events.back() == e) { tier->announce_events.resize(std::size(tier->announce_events) - 1); } tier_update_announce_priority(tier); } void tier_announce_event_push(tr_tier* tier, tr_announce_event e, time_t announce_at) { TR_ASSERT(tier != nullptr); tr_logAddTrace_tier_announce_queue(tier); tr_logAddTraceTier(tier, fmt::format("queued '{}'", tr_announce_event_get_string(e))); auto& events = tier->announce_events; if (!std::empty(events)) { /* special case #1: if we're adding a "stopped" event, * dump everything leading up to it except "completed" */ if (e == TR_ANNOUNCE_EVENT_STOPPED) { bool const has_completed = std::count(std::begin(events), std::end(events), TR_ANNOUNCE_EVENT_COMPLETED) != 0; events.clear(); if (has_completed) { events.push_back(TR_ANNOUNCE_EVENT_COMPLETED); } } /* special case #2: dump all empty strings leading up to this event */ tier_announce_remove_trailing(tier, TR_ANNOUNCE_EVENT_NONE); /* special case #3: no consecutive duplicates */ tier_announce_remove_trailing(tier, e); } /* add it */ events.push_back(e); tier->announceAt = announce_at; tier_update_announce_priority(tier); tr_logAddTrace_tier_announce_queue(tier); tr_logAddTraceTier(tier, fmt::format("announcing in {} seconds", difftime(announce_at, tr_time()))); } auto tier_announce_event_pull(tr_tier* tier) { auto const e = tier->announce_events.front(); tier->announce_events.pop_front(); tier_update_announce_priority(tier); return e; } bool isUnregistered(char const* errmsg) { auto const lower = tr_strlower(errmsg != nullptr ? errmsg : ""); auto constexpr Keys = std::array{ "unregistered torrent"sv, "torrent not registered"sv }; return std::any_of(std::begin(Keys), std::end(Keys), [&lower](auto const& key) { return tr_strv_contains(lower, key); }); } void on_announce_error(tr_tier* tier, char const* err, tr_announce_event e) { using namespace announce_helpers; auto* current_tracker = tier->currentTracker(); std::string const announce_url = current_tracker != nullptr ? tr_urlTrackerLogName(current_tracker->announce_url) : "nullptr"; /* increment the error count */ if (current_tracker != nullptr) { ++current_tracker->consecutive_failures; } /* set the error message */ tier->last_announce_str = err; /* switch to the next tracker */ current_tracker = tier->useNextTracker(); if (isUnregistered(err)) { tr_logAddErrorTier( tier, fmt::format(_("Announce error: {error} ({url})"), fmt::arg("error", err), fmt::arg("url", announce_url))); } else { /* schedule a reannounce */ auto const interval = current_tracker->getRetryInterval(); tr_logAddWarnTier( tier, fmt::format( tr_ngettext( "Announce error: {error} (Retrying in {count} second) ({url})", "Announce error: {error} (Retrying in {count} seconds) ({url})", interval), fmt::arg("error", err), fmt::arg("count", interval), fmt::arg("url", announce_url))); tier_announce_event_push(tier, e, tr_time() + interval); } } [[nodiscard]] tr_announce_request create_announce_request( tr_announcer_impl const* const announcer, tr_torrent* const tor, tr_tier const* const tier, tr_announce_event const event) { auto const* const current_tracker = tier->currentTracker(); TR_ASSERT(current_tracker != nullptr); auto req = tr_announce_request{}; req.port = announcer->session->advertisedPeerPort(); req.announce_url = current_tracker->announce_url; req.tracker_id = current_tracker->tracker_id; req.info_hash = tor->info_hash(); req.peer_id = tor->peer_id(); req.up = tier->byteCounts[TR_ANN_UP]; req.down = tier->byteCounts[TR_ANN_DOWN]; req.corrupt = tier->byteCounts[TR_ANN_CORRUPT]; req.leftUntilComplete = tor->has_metainfo() ? tor->total_size() - tor->has_total() : INT64_MAX; req.event = event; req.numwant = event == TR_ANNOUNCE_EVENT_STOPPED ? 0 : Numwant; req.key = tor->announce_key(); req.partial_seed = tor->is_partial_seed(); req.log_name = tier->buildLogName(); return req; } [[nodiscard]] tr_tier* getTier(tr_announcer_impl* announcer, tr_sha1_digest_t const& info_hash, int tier_id) { if (announcer == nullptr) { return nullptr; } auto* const tor = announcer->session->torrents().get(info_hash); if (tor == nullptr || tor->torrent_announcer == nullptr) { return nullptr; } return tor->torrent_announcer->getTier(tier_id); } } // namespace announce_helpers void torrentAddAnnounce(tr_torrent* tor, tr_announce_event e, time_t announce_at) { using namespace announce_helpers; // tell each tier to announce for (auto& tier : tor->torrent_announcer->tiers) { tier_announce_event_push(&tier, e, announce_at); } } } // namespace void tr_announcer_impl::onAnnounceDone( int tier_id, tr_announce_event event, bool is_running_on_success, tr_announce_response const& response) { using namespace announce_helpers; using namespace publish_helpers; auto* const tier = getTier(this, response.info_hash, tier_id); if (tier == nullptr) { return; } auto const now = tr_time(); tr_logAddTraceTier( tier, fmt::format( "Got announce response: " "connected:{} " "timeout:{} " "seeders:{} " "leechers:{} " "downloads:{} " "interval:{} " "min_interval:{} " "tracker_id_str:{} " "pex:{} " "pex6:{} " "err:{} " "warn:{}", response.did_connect, response.did_timeout, response.seeders.value_or(-1), response.leechers.value_or(-1), response.downloads.value_or(-1), response.interval, response.min_interval, (!std::empty(response.tracker_id) ? response.tracker_id.c_str() : "none"), std::size(response.pex), std::size(response.pex6), (!std::empty(response.errmsg) ? response.errmsg.c_str() : "none"), (!std::empty(response.warning) ? response.warning.c_str() : "none"))); tier->lastAnnounceTime = now; tier->lastAnnounceTimedOut = response.did_timeout; tier->lastAnnounceSucceeded = false; tier->isAnnouncing = false; tier->manualAnnounceAllowedAt = now + tier->announceMinIntervalSec; if (response.external_ip) { session->set_global_address(*response.external_ip); } if (!response.did_connect) { on_announce_error(tier, _("Could not connect to tracker"), event); } else if (response.did_timeout) { on_announce_error(tier, _("Tracker did not respond"), event); } else if (!std::empty(response.errmsg)) { /* If the torrent's only tracker returned an error, publish it. Don't bother publishing if there are other trackers -- it's all too common for people to load up dozens of dead trackers in a torrent's metainfo... */ if (std::size(tier->tor->announce_list()) < 2U) { publishError(tier, response.errmsg); } on_announce_error(tier, response.errmsg.c_str(), event); } else { auto const is_stopped = event == TR_ANNOUNCE_EVENT_STOPPED; auto scrape_fields = uint8_t{}; publishErrorClear(tier); auto* const tracker = tier->currentTracker(); if (tracker != nullptr) { tracker->consecutive_failures = 0; if (tracker->set_seeder_count(response.seeders)) { ++scrape_fields; } if (tracker->set_leecher_count(response.leechers)) { ++scrape_fields; } if (tracker->set_download_count(response.downloads)) { ++scrape_fields; } if (!std::empty(response.tracker_id)) { tracker->tracker_id = response.tracker_id; } } if (auto const& warning = response.warning; !std::empty(warning)) { tier->last_announce_str = warning; tr_logAddTraceTier(tier, fmt::format("tracker gave '{}'", warning)); publishWarning(tier, warning); } else { tier->last_announce_str = _("Success"); } if (response.min_interval != 0) { tier->announceMinIntervalSec = response.min_interval; } if (response.interval != 0) { tier->announceIntervalSec = response.interval; } if (!std::empty(response.pex)) { publishPeersPex(tier, response.pex); } if (!std::empty(response.pex6)) { publishPeersPex(tier, response.pex6); } publishPeerCounts(tier, response.seeders, response.leechers); tier->isRunning = is_running_on_success; /* if the tracker included scrape fields in its announce response, then a separate scrape isn't needed */ if (scrape_fields >= 3U || (scrape_fields >= 1U && tracker->scrape_info == nullptr)) { tr_logAddTraceTier( tier, fmt::format( "Announce response has scrape info; bumping next scrape to {} seconds from now.", tier->scrapeIntervalSec)); tier->scheduleNextScrape(); tier->lastScrapeTime = now; tier->lastScrapeSucceeded = true; } else if (tier->lastScrapeTime + tier->scrapeIntervalSec <= now) { tier->scrapeSoon(); } tier->lastAnnounceSucceeded = true; tier->lastAnnouncePeerCount = std::size(response.pex) + std::size(response.pex6); if (is_stopped) { /* now that we've successfully stopped the torrent, * we can reset the up/down/corrupt count we've kept * for this tracker */ tier->byteCounts[TR_ANN_UP] = 0; tier->byteCounts[TR_ANN_DOWN] = 0; tier->byteCounts[TR_ANN_CORRUPT] = 0; } if (!is_stopped && std::empty(tier->announce_events)) { /* the queue is empty, so enqueue a periodic update */ int const i = tier->announceIntervalSec; tr_logAddTraceTier(tier, fmt::format("Sending periodic reannounce in {} seconds", i)); tier_announce_event_push(tier, TR_ANNOUNCE_EVENT_NONE, now + i); } } } void tr_announcer_impl::startTorrent(tr_torrent* tor) { torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STARTED, tr_time()); } void tr_announcerManualAnnounce(tr_torrent* tor) { torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_NONE, tr_time()); } void tr_announcer_impl::stopTorrent(tr_torrent* tor) { torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STOPPED, tr_time()); } void tr_announcerTorrentCompleted(tr_torrent* tor) { torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_COMPLETED, tr_time()); } void tr_announcerChangeMyPort(tr_torrent* tor) { torrentAddAnnounce(tor, TR_ANNOUNCE_EVENT_STARTED, tr_time()); } // --- void tr_announcerAddBytes(tr_torrent* tor, int type, uint32_t n_bytes) { TR_ASSERT(tr_isTorrent(tor)); TR_ASSERT(type == TR_ANN_UP || type == TR_ANN_DOWN || type == TR_ANN_CORRUPT); for (auto& tier : tor->torrent_announcer->tiers) { tier.byteCounts[type] += n_bytes; } } // --- void tr_announcer_impl::removeTorrent(tr_torrent* tor) { using namespace announce_helpers; // FIXME(ckerr) auto* const ta = tor->torrent_announcer; if (ta == nullptr) { return; } for (auto const& tier : ta->tiers) { if (tier.isRunning && tier.lastAnnounceSucceeded) { stops_.emplace(create_announce_request(this, tor, &tier, TR_ANNOUNCE_EVENT_STOPPED)); } } tor->torrent_announcer = nullptr; delete ta; } // --- SCRAPE namespace { namespace on_scrape_done_helpers { [[nodiscard]] TR_CONSTEXPR20 bool multiscrape_too_big(std::string_view errmsg) { /* Found a tracker that returns some bespoke string for this case? Add your patch here and open a PR */ auto too_long_errors = std::array{ "Bad Request", "GET string too long", "Request-URI Too Long", }; return std::any_of( std::begin(too_long_errors), std::end(too_long_errors), [&errmsg](auto const& substr) { return tr_strv_contains(errmsg, substr); }); } void on_scrape_error(tr_session const* /*session*/, tr_tier* tier, char const* errmsg) { if (auto* const current_tracker = tier->currentTracker(); current_tracker != nullptr) { ++current_tracker->consecutive_failures; tr_logAddDebugTier( tier, fmt::format( "Tracker '{}' scrape error: {} (Can retry in {} seconds)", current_tracker->announce_parsed.authority, errmsg, current_tracker->getRetryInterval())); } // set the error message tier->last_scrape_str = errmsg != nullptr ? errmsg : ""; tier->lastScrapeSucceeded = false; // switch to the next tracker if (auto* const current_tracker = tier->useNextTracker(); current_tracker != nullptr) { // schedule a rescrape tier->scheduleNextScrape(current_tracker->getRetryInterval()); } } void checkMultiscrapeMax(tr_announcer_impl* announcer, tr_scrape_response const& response) { if (!multiscrape_too_big(response.errmsg)) { return; } auto const& url = response.scrape_url; auto* const scrape_info = announcer->scrape_info(url); if (scrape_info == nullptr) { return; } // Lower the max only if it hasn't already lowered for a similar // error. So if N parallel multiscrapes all have the same `max` // and error out, lower the value once for that batch, not N times. int& multiscrape_max = scrape_info->multiscrape_max; if (multiscrape_max < response.row_count) { return; } int const n = std::max(1, int{ multiscrape_max - TrMultiscrapeStep }); if (multiscrape_max != n) { // don't log the full URL, since that might have a personal announce id tr_logAddDebug(fmt::format("Reducing multiscrape max to {:d}", n), tr_urlTrackerLogName(url)); multiscrape_max = n; } } } // namespace on_scrape_done_helpers } // namespace void tr_announcer_impl::onScrapeDone(tr_scrape_response const& response) { using namespace on_scrape_done_helpers; using namespace publish_helpers; auto const now = tr_time(); for (int i = 0; i < response.row_count; ++i) { auto const& row = response.rows[i]; auto* const tor = session->torrents().get(row.info_hash); if (tor == nullptr) { continue; } auto* const tier = tor->torrent_announcer->getTierFromScrape(response.scrape_url); if (tier == nullptr) { continue; } tr_logAddTraceTier( tier, fmt::format( "scraped url:{} " " -- " "did_connect:{} " "did_timeout:{} " "seeders:{} " "leechers:{} " "downloads:{} " "downloaders:{} " "min_request_interval:{} " "err:{} ", response.scrape_url.sv(), response.did_connect, response.did_timeout, row.seeders.value_or(-1), row.leechers.value_or(-1), row.downloads.value_or(-1), row.downloaders.value_or(-1), response.min_request_interval, std::empty(response.errmsg) ? "none"sv : response.errmsg)); tier->isScraping = false; tier->lastScrapeTime = now; tier->lastScrapeSucceeded = false; tier->lastScrapeTimedOut = response.did_timeout; if (!response.did_connect) { on_scrape_error(session, tier, _("Could not connect to tracker")); } else if (response.did_timeout) { on_scrape_error(session, tier, _("Tracker did not respond")); } else if (!std::empty(response.errmsg)) { on_scrape_error(session, tier, response.errmsg.c_str()); } else { tier->lastScrapeSucceeded = true; tier->scrapeIntervalSec = std::max(int{ DefaultScrapeIntervalSec }, response.min_request_interval); tier->scheduleNextScrape(); tr_logAddTraceTier(tier, fmt::format("Scrape successful. Rescraping in {} seconds.", tier->scrapeIntervalSec)); if (tr_tracker* const tracker = tier->currentTracker(); tracker != nullptr) { tracker->set_seeder_count(row.seeders); tracker->set_leecher_count(row.leechers); tracker->set_download_count(row.downloads); tracker->set_downloader_count(row.downloaders); tracker->consecutive_failures = 0; } publishPeerCounts(tier, row.seeders, row.leechers); } } checkMultiscrapeMax(this, response); } namespace { void multiscrape(tr_announcer_impl* announcer, std::vector const& tiers) { auto const now = tr_time(); auto requests = std::array{}; auto request_count = size_t{}; // batch as many info_hashes into a request as we can for (auto* tier : tiers) { auto const* const current_tracker = tier->currentTracker(); TR_ASSERT(current_tracker != nullptr); if (current_tracker == nullptr) { continue; } auto const* const scrape_info = current_tracker->scrape_info; TR_ASSERT(scrape_info != nullptr); if (scrape_info == nullptr) { continue; } bool found = false; /* if there's a request with this scrape URL and a free slot, use it */ for (size_t j = 0; !found && j < request_count; ++j) { auto* const req = &requests[j]; if (req->info_hash_count >= scrape_info->multiscrape_max) { continue; } if (scrape_info->scrape_url != req->scrape_url) { continue; } req->info_hash[req->info_hash_count] = tier->tor->info_hash(); ++req->info_hash_count; tier->isScraping = true; tier->lastScrapeStartTime = now; found = true; } /* otherwise, if there's room for another request, build a new one */ if (!found && request_count < MaxScrapesPerUpkeep) { auto* const req = &requests[request_count]; req->scrape_url = scrape_info->scrape_url; req->log_name = tier->buildLogName(); req->info_hash[req->info_hash_count] = tier->tor->info_hash(); ++req->info_hash_count; tier->isScraping = true; tier->lastScrapeStartTime = now; ++request_count; } } /* send the requests we just built */ for (size_t i = 0; i < request_count; ++i) { announcer->scrape( requests[i], [session = announcer->session, announcer](tr_scrape_response const& response) { if (session->announcer_) { announcer->onScrapeDone(response); } }); } } namespace upkeep_helpers { int compareAnnounceTiers(tr_tier const* a, tr_tier const* b) { /* prefer higher-priority events */ if (auto const val = tr_compare_3way(a->announce_event_priority, b->announce_event_priority); val != 0) { return -val; } /* prefer swarms where we might upload */ if (auto const val = tr_compare_3way(a->countDownloaders(), b->countDownloaders()); val != 0) { return -val; } /* prefer swarms where we might download */ if (auto const val = tr_compare_3way(a->tor->is_done(), b->tor->is_done()); val != 0) { return val; } /* prefer larger stats, to help ensure stats get recorded when stopping on shutdown */ if (auto const val = tr_compare_3way( a->byteCounts[TR_ANN_UP] + a->byteCounts[TR_ANN_DOWN], b->byteCounts[TR_ANN_UP] + b->byteCounts[TR_ANN_DOWN]); val != 0) { return -val; } // announcements that have been waiting longer go first if (auto const val = tr_compare_3way(a->announceAt, b->announceAt); val != 0) { return val; } // the tiers are effectively equal priority, but add an arbitrary // differentiation because ptrArray sorted mode hates equal items. return tr_compare_3way(a, b); } void tierAnnounce(tr_announcer_impl* announcer, tr_tier* tier) { using namespace announce_helpers; TR_ASSERT(!tier->isAnnouncing); TR_ASSERT(!std::empty(tier->announce_events)); TR_ASSERT(tier->currentTracker() != nullptr); auto const now = tr_time(); tr_torrent* tor = tier->tor; auto const event = tier_announce_event_pull(tier); auto const req = create_announce_request(announcer, tor, tier, event); tier->isAnnouncing = true; tier->lastAnnounceStartTime = now; auto tier_id = tier->id; auto is_running_on_success = tor->is_running(); announcer->announce( req, [session = announcer->session, announcer, tier_id, event, is_running_on_success](tr_announce_response const& response) { if (session->announcer_) { announcer->onAnnounceDone(tier_id, event, is_running_on_success, response); } }); } void scrapeAndAnnounceMore(tr_announcer_impl* announcer) { auto const now = tr_time(); /* build a list of tiers that need to be announced */ auto announce_me = std::vector{}; auto scrape_me = std::vector{}; for (auto* const tor : announcer->session->torrents()) { for (auto& tier : tor->torrent_announcer->tiers) { if (tier.needsToAnnounce(now)) { announce_me.push_back(&tier); } if (tier.needsToScrape(now)) { scrape_me.push_back(&tier); } } } /* First, scrape what we can. We handle scrapes first because * we can work through that queue much faster than announces * (thanks to multiscrape) _and_ the scrape responses will tell * us which swarms are interesting and should be announced next. */ multiscrape(announcer, scrape_me); /* Second, announce what we can. If there aren't enough slots * available, use compareAnnounceTiers to prioritize. */ if (announce_me.size() > MaxAnnouncesPerUpkeep) { std::partial_sort( std::begin(announce_me), std::begin(announce_me) + MaxAnnouncesPerUpkeep, std::end(announce_me), [](auto const* a, auto const* b) { return compareAnnounceTiers(a, b) < 0; }); announce_me.resize(MaxAnnouncesPerUpkeep); } for (auto*& tier : announce_me) { tr_logAddTraceTier(tier, "Announcing to tracker"); tierAnnounce(announcer, tier); } } } // namespace upkeep_helpers } // namespace void tr_announcer_impl::upkeep() { using namespace upkeep_helpers; auto const lock = session->unique_lock(); // maybe send out some "stopped" messages for closed torrents flushCloseMessages(); // maybe kick off some scrapes / announces whose time has come if (!is_shutting_down_) { scrapeAndAnnounceMore(this); } announcer_udp_.upkeep(); } // --- namespace { namespace tracker_view_helpers { [[nodiscard]] auto trackerView(tr_torrent const& tor, size_t tier_index, tr_tier const& tier, tr_tracker const& tracker) { auto const& announce = tracker.announce_parsed; auto const now = tr_time(); auto view = tr_tracker_view{}; *fmt::format_to_n( std::data(view.host_and_port), std::size(view.host_and_port) - 1U, "{:s}:{:d}", announce.host, announce.port) .out = '\0'; *fmt::format_to_n(std::data(view.sitename), std::size(view.sitename) - 1U, "{:s}", announce.sitename).out = '\0'; view.announce = tracker.announce_url.c_str(); view.scrape = tracker.scrape_info == nullptr ? "" : tracker.scrape_info->scrape_url.c_str(); view.id = tracker.id; view.tier = tier_index; view.isBackup = &tracker != tier.currentTracker(); view.lastScrapeStartTime = tier.lastScrapeStartTime; view.seederCount = tracker.seeder_count().value_or(-1); view.leecherCount = tracker.leecher_count().value_or(-1); view.downloadCount = tracker.download_count().value_or(-1); if (view.isBackup) { view.scrapeState = TR_TRACKER_INACTIVE; view.announceState = TR_TRACKER_INACTIVE; view.nextScrapeTime = 0; view.nextAnnounceTime = 0; } else { view.hasScraped = tier.lastScrapeTime != 0; if (view.hasScraped) { view.lastScrapeTime = tier.lastScrapeTime; view.lastScrapeSucceeded = tier.lastScrapeSucceeded; view.lastScrapeTimedOut = tier.lastScrapeTimedOut; auto& buf = view.lastScrapeResult; *fmt::format_to_n(buf, sizeof(buf) - 1, "{:s}", tier.last_scrape_str).out = '\0'; } if (tier.isScraping) { view.scrapeState = TR_TRACKER_ACTIVE; } else if (tier.scrapeAt == 0) { view.scrapeState = TR_TRACKER_INACTIVE; } else if (tier.scrapeAt > now) { view.scrapeState = TR_TRACKER_WAITING; view.nextScrapeTime = tier.scrapeAt; } else { view.scrapeState = TR_TRACKER_QUEUED; } view.lastAnnounceStartTime = tier.lastAnnounceStartTime; view.hasAnnounced = tier.lastAnnounceTime != 0; if (view.hasAnnounced) { view.lastAnnounceTime = tier.lastAnnounceTime; view.lastAnnounceSucceeded = tier.lastAnnounceSucceeded; view.lastAnnounceTimedOut = tier.lastAnnounceTimedOut; view.lastAnnouncePeerCount = tier.lastAnnouncePeerCount; auto& buf = view.lastAnnounceResult; *fmt::format_to_n(buf, sizeof(buf) - 1, "{:s}", tier.last_announce_str).out = '\0'; } if (tier.isAnnouncing) { view.announceState = TR_TRACKER_ACTIVE; } else if (!tor.is_running() || tier.announceAt == 0) { view.announceState = TR_TRACKER_INACTIVE; } else if (tier.announceAt > now) { view.announceState = TR_TRACKER_WAITING; view.nextAnnounceTime = tier.announceAt; } else { view.announceState = TR_TRACKER_QUEUED; } } return view; } } // namespace tracker_view_helpers } // namespace size_t tr_announcerTrackerCount(tr_torrent const* tor) { TR_ASSERT(tr_isTorrent(tor)); TR_ASSERT(tor->torrent_announcer != nullptr); auto const& tiers = tor->torrent_announcer->tiers; return std::accumulate( std::begin(tiers), std::end(tiers), size_t{}, [](size_t acc, auto const& cur) { return acc + std::size(cur.trackers); }); } tr_tracker_view tr_announcerTracker(tr_torrent const* tor, size_t nth) { using namespace tracker_view_helpers; TR_ASSERT(tr_isTorrent(tor)); TR_ASSERT(tor->torrent_announcer != nullptr); auto tier_index = size_t{ 0 }; auto tracker_index = size_t{ 0 }; for (auto const& tier : tor->torrent_announcer->tiers) { for (auto const& tracker : tier.trackers) { if (tracker_index == nth) { return trackerView(*tor, tier_index, tier, tracker); } ++tracker_index; } ++tier_index; } TR_ASSERT(false); return {}; } // --- // called after the torrent's announceList was rebuilt -- // so announcer needs to update the tr_tier / tr_trackers to match void tr_announcer_impl::resetTorrent(tr_torrent* tor) { using namespace announce_helpers; // make a new tr_announcer_tier auto* const older = tor->torrent_announcer; tor->torrent_announcer = new tr_torrent_announcer{ this, tor }; auto* const newer = tor->torrent_announcer; // copy the tracker counts into the new replacementa if (older != nullptr) { for (auto& new_tier : newer->tiers) { for (auto& new_tracker : new_tier.trackers) { tr_tier const* old_tier = nullptr; tr_tracker const* old_tracker = nullptr; if (older->findTracker(new_tracker.announce_url, &old_tier, &old_tracker)) { new_tracker.set_seeder_count(old_tracker->seeder_count()); new_tracker.set_leecher_count(old_tracker->leecher_count()); new_tracker.set_download_count(old_tracker->download_count()); new_tracker.set_downloader_count(old_tracker->downloader_count()); new_tier.announce_events = old_tier->announce_events; new_tier.announce_event_priority = old_tier->announce_event_priority; auto const* const old_current = old_tier->currentTracker(); new_tier.current_tracker_index_ = old_current == nullptr ? std::nullopt : new_tier.indexOf(old_current->announce_url); } } } } // kickstart any tiers that didn't get started if (tor->is_running()) { auto const now = tr_time(); for (auto& tier : newer->tiers) { if (!tier.current_tracker_index_) { tier.useNextTracker(); tier_announce_event_push(&tier, TR_ANNOUNCE_EVENT_STARTED, now); } } } delete older; }