mirror of
https://github.com/transmission/transmission
synced 2024-12-27 18:18:10 +00:00
c472cbd88d
* refactor: [[maybe_unused]] iff arg _might_ be used If the arg is never used, comment out its name. If the arg is _sometimes_ used e.g. with ifdefs, use [[maybe_unused]].
1940 lines
56 KiB
C++
1940 lines
56 KiB
C++
/*
|
|
* This file Copyright (C) 2010-2014 Mnemosyne LLC
|
|
*
|
|
* It may be used under the GNU GPL versions 2 or 3
|
|
* or any future license endorsed by Mnemosyne LLC.
|
|
*
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <climits> /* INT_MAX */
|
|
#include <cstdio>
|
|
#include <cstdlib> /* qsort() */
|
|
#include <cstring> /* strcmp(), memcpy(), strncmp() */
|
|
#include <map>
|
|
#include <set>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include <event2/buffer.h>
|
|
#include <event2/event.h> /* evtimer */
|
|
|
|
#define LIBTRANSMISSION_ANNOUNCER_MODULE
|
|
|
|
#include "transmission.h"
|
|
#include "announcer.h"
|
|
#include "announcer-common.h"
|
|
#include "crypto-utils.h" /* tr_rand_int(), tr_rand_int_weak() */
|
|
#include "log.h"
|
|
#include "peer-mgr.h" /* tr_peerMgrCompactToPex() */
|
|
#include "session.h"
|
|
#include "torrent.h"
|
|
#include "tr-assert.h"
|
|
#include "utils.h"
|
|
|
|
struct tr_tier;
|
|
|
|
static void tier_build_log_name(struct tr_tier const* tier, char* buf, size_t buflen);
|
|
|
|
#define dbgmsg(tier, ...) \
|
|
do \
|
|
{ \
|
|
if (tr_logGetDeepEnabled()) \
|
|
{ \
|
|
char name[128]; \
|
|
tier_build_log_name(tier, name, TR_N_ELEMENTS(name)); \
|
|
tr_logAddDeep(__FILE__, __LINE__, name, __VA_ARGS__); \
|
|
} \
|
|
} while (0)
|
|
|
|
/* unless the tracker says otherwise, rescrape this frequently */
|
|
static auto constexpr DefaultScrapeIntervalSec = int{ 60 * 30 };
|
|
/* unless the tracker says otherwise, this is the announce interval */
|
|
static auto constexpr DefaultAnnounceIntervalSec = int{ 60 * 10 };
|
|
/* unless the tracker says otherwise, this is the announce min_interval */
|
|
static auto constexpr DefaultAnnounceMinIntervalSec = int{ 60 * 2 };
|
|
/* the value of the 'numwant' argument passed in tracker requests. */
|
|
static auto constexpr Numwant = int{ 80 };
|
|
|
|
/* how often to announce & scrape */
|
|
static auto constexpr UpkeepIntervalMsec = int{ 500 };
|
|
static auto constexpr MaxAnnouncesPerUpkeep = int{ 20 };
|
|
static auto constexpr MaxScrapesPerUpkeep = int{ 20 };
|
|
|
|
/* this is how often to call the UDP tracker upkeep */
|
|
static auto constexpr TauUpkeepIntervalSecs = int{ 5 };
|
|
|
|
/* how many infohashes to remove when we get a scrape-too-long error */
|
|
static auto constexpr TrMultiscrapeStep = int{ 5 };
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
char const* tr_announce_event_get_string(tr_announce_event e)
|
|
{
|
|
switch (e)
|
|
{
|
|
case TR_ANNOUNCE_EVENT_COMPLETED:
|
|
return "completed";
|
|
|
|
case TR_ANNOUNCE_EVENT_STARTED:
|
|
return "started";
|
|
|
|
case TR_ANNOUNCE_EVENT_STOPPED:
|
|
return "stopped";
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
struct StopsCompare
|
|
{
|
|
int compare(tr_announce_request const* a, tr_announce_request const* b) const
|
|
{
|
|
// primary key: volume of data transferred
|
|
auto ax = a->up + a->down;
|
|
auto bx = b->up + b->down;
|
|
if (ax != bx)
|
|
{
|
|
return ax > bx ? -1 : 1;
|
|
}
|
|
|
|
// secondary key: the torrent's info_hash
|
|
auto const i = memcmp(a->info_hash, b->info_hash, SHA_DIGEST_LENGTH);
|
|
if (i != 0)
|
|
{
|
|
return i;
|
|
}
|
|
|
|
// tertiary key: the tracker's announce url
|
|
return tr_strcmp0(a->url, b->url);
|
|
}
|
|
|
|
bool operator()(tr_announce_request const* a, tr_announce_request const* b) const
|
|
{
|
|
return compare(a, b) < 0;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
struct tr_scrape_info
|
|
{
|
|
std::string const url;
|
|
|
|
int multiscrape_max;
|
|
|
|
tr_scrape_info(std::string url_in, int const multiscrape_max_in)
|
|
: url{ std::move(url_in) }
|
|
, multiscrape_max{ multiscrape_max_in }
|
|
{
|
|
}
|
|
};
|
|
|
|
/**
|
|
* "global" (per-tr_session) fields
|
|
*/
|
|
struct tr_announcer
|
|
{
|
|
std::set<tr_announce_request*, StopsCompare> stops;
|
|
std::map<std::string, tr_scrape_info> scrape_info;
|
|
|
|
tr_session* session;
|
|
struct event* upkeepTimer;
|
|
int key;
|
|
time_t tauUpkeepAt;
|
|
};
|
|
|
|
static struct tr_scrape_info* tr_announcerGetScrapeInfo(struct tr_announcer* announcer, std::string const& url)
|
|
{
|
|
struct tr_scrape_info* info = nullptr;
|
|
|
|
if (!std::empty(url))
|
|
{
|
|
auto& scrapes = announcer->scrape_info;
|
|
auto const it = scrapes.try_emplace(url, url, TR_MULTISCRAPE_MAX);
|
|
info = &it.first->second;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
static void onUpkeepTimer(evutil_socket_t fd, short what, void* vannouncer);
|
|
|
|
void tr_announcerInit(tr_session* session)
|
|
{
|
|
TR_ASSERT(tr_isSession(session));
|
|
|
|
auto* a = new tr_announcer{};
|
|
a->key = tr_rand_int(INT_MAX);
|
|
a->session = session;
|
|
a->upkeepTimer = evtimer_new(session->event_base, onUpkeepTimer, a);
|
|
tr_timerAddMsec(a->upkeepTimer, UpkeepIntervalMsec);
|
|
|
|
session->announcer = a;
|
|
}
|
|
|
|
static void flushCloseMessages(tr_announcer* announcer);
|
|
|
|
void tr_announcerClose(tr_session* session)
|
|
{
|
|
tr_announcer* announcer = session->announcer;
|
|
|
|
flushCloseMessages(announcer);
|
|
|
|
tr_tracker_udp_start_shutdown(session);
|
|
|
|
event_free(announcer->upkeepTimer);
|
|
announcer->upkeepTimer = nullptr;
|
|
|
|
session->announcer = nullptr;
|
|
delete announcer;
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
/* a row in tr_tier's list of trackers */
|
|
struct tr_tracker
|
|
{
|
|
char* key;
|
|
char* announce;
|
|
struct tr_scrape_info* scrape_info;
|
|
|
|
char* tracker_id_str;
|
|
|
|
int seederCount;
|
|
int leecherCount;
|
|
int downloadCount;
|
|
int downloaderCount;
|
|
|
|
int consecutiveFailures;
|
|
|
|
uint32_t id;
|
|
};
|
|
|
|
/* format: host+':'+ port */
|
|
static char* getKey(char const* url)
|
|
{
|
|
char* scheme = nullptr;
|
|
char* host = nullptr;
|
|
int port = 0;
|
|
|
|
tr_urlParse(url, TR_BAD_SIZE, &scheme, &host, &port, nullptr);
|
|
char* const ret = tr_strdup_printf(
|
|
"%s://%s:%d",
|
|
scheme != nullptr ? scheme : "invalid",
|
|
host != nullptr ? host : "invalid",
|
|
port);
|
|
|
|
tr_free(host);
|
|
tr_free(scheme);
|
|
return ret;
|
|
}
|
|
|
|
static void trackerConstruct(tr_announcer* announcer, tr_tracker* tracker, tr_tracker_info const* inf)
|
|
{
|
|
memset(tracker, 0, sizeof(tr_tracker));
|
|
tracker->key = getKey(inf->announce);
|
|
tracker->announce = tr_strdup(inf->announce);
|
|
tracker->scrape_info = inf->scrape == nullptr ? nullptr : tr_announcerGetScrapeInfo(announcer, inf->scrape);
|
|
tracker->id = inf->id;
|
|
tracker->seederCount = -1;
|
|
tracker->leecherCount = -1;
|
|
tracker->downloadCount = -1;
|
|
}
|
|
|
|
static void trackerDestruct(tr_tracker* tracker)
|
|
{
|
|
tr_free(tracker->tracker_id_str);
|
|
tr_free(tracker->announce);
|
|
tr_free(tracker->key);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
struct tr_torrent_tiers;
|
|
|
|
/** @brief A group of trackers in a single tier, as per the multitracker spec */
|
|
struct tr_tier
|
|
{
|
|
/* number of up/down/corrupt bytes since the last time we sent an
|
|
* "event=stopped" message that was acknowledged by the tracker */
|
|
uint64_t byteCounts[3];
|
|
|
|
tr_tracker* trackers;
|
|
int tracker_count;
|
|
tr_tracker* currentTracker;
|
|
int currentTrackerIndex;
|
|
|
|
tr_torrent* tor;
|
|
|
|
time_t scrapeAt;
|
|
time_t lastScrapeStartTime;
|
|
time_t lastScrapeTime;
|
|
bool lastScrapeSucceeded;
|
|
bool lastScrapeTimedOut;
|
|
|
|
time_t announceAt;
|
|
time_t manualAnnounceAllowedAt;
|
|
time_t lastAnnounceStartTime;
|
|
time_t lastAnnounceTime;
|
|
bool lastAnnounceSucceeded;
|
|
bool lastAnnounceTimedOut;
|
|
|
|
tr_announce_event* announce_events;
|
|
int announce_event_priority;
|
|
int announce_event_count;
|
|
int announce_event_alloc;
|
|
|
|
/* unique lookup key */
|
|
int key;
|
|
|
|
int scrapeIntervalSec;
|
|
int announceIntervalSec;
|
|
int announceMinIntervalSec;
|
|
|
|
int lastAnnouncePeerCount;
|
|
|
|
bool isRunning;
|
|
bool isAnnouncing;
|
|
bool isScraping;
|
|
bool wasCopied;
|
|
|
|
char lastAnnounceStr[128];
|
|
char lastScrapeStr[128];
|
|
};
|
|
|
|
static time_t get_next_scrape_time(tr_session const* session, tr_tier const* tier, int interval)
|
|
{
|
|
/* Maybe don't scrape paused torrents */
|
|
if (!tier->isRunning && !session->scrapePausedTorrents)
|
|
{
|
|
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. */
|
|
time_t const now = tr_time();
|
|
time_t ret = now + interval;
|
|
while (ret % 10 != 0)
|
|
{
|
|
++ret;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void tierConstruct(tr_tier* tier, tr_torrent* tor)
|
|
{
|
|
static int nextKey = 1;
|
|
|
|
memset(tier, 0, sizeof(tr_tier));
|
|
|
|
tier->key = nextKey++;
|
|
tier->currentTrackerIndex = -1;
|
|
tier->scrapeIntervalSec = DefaultScrapeIntervalSec;
|
|
tier->announceIntervalSec = DefaultAnnounceIntervalSec;
|
|
tier->announceMinIntervalSec = DefaultAnnounceMinIntervalSec;
|
|
tier->scrapeAt = get_next_scrape_time(tor->session, tier, 0);
|
|
tier->tor = tor;
|
|
}
|
|
|
|
static void tierDestruct(tr_tier* tier)
|
|
{
|
|
tr_free(tier->announce_events);
|
|
}
|
|
|
|
static void tier_build_log_name(tr_tier const* tier, char* buf, size_t buflen)
|
|
{
|
|
tr_snprintf(
|
|
buf,
|
|
buflen,
|
|
"[%s---%s]",
|
|
(tier != nullptr && tier->tor != nullptr) ? tr_torrentName(tier->tor) : "?",
|
|
(tier != nullptr && tier->currentTracker != nullptr) ? tier->currentTracker->key : "?");
|
|
}
|
|
|
|
static void tierIncrementTracker(tr_tier* tier)
|
|
{
|
|
/* move our index to the next tracker in the tier */
|
|
int const i = tier->currentTracker == nullptr ? 0 : (tier->currentTrackerIndex + 1) % tier->tracker_count;
|
|
tier->currentTrackerIndex = i;
|
|
tier->currentTracker = &tier->trackers[i];
|
|
|
|
/* reset some of the tier's fields */
|
|
tier->scrapeIntervalSec = DefaultScrapeIntervalSec;
|
|
tier->announceIntervalSec = DefaultAnnounceIntervalSec;
|
|
tier->announceMinIntervalSec = DefaultAnnounceMinIntervalSec;
|
|
tier->isAnnouncing = false;
|
|
tier->isScraping = false;
|
|
tier->lastAnnounceStartTime = 0;
|
|
tier->lastScrapeStartTime = 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_tiers
|
|
{
|
|
tr_tier* tiers;
|
|
int tier_count;
|
|
|
|
tr_tracker* trackers;
|
|
int tracker_count;
|
|
|
|
tr_tracker_callback callback;
|
|
void* callbackData;
|
|
};
|
|
|
|
static tr_torrent_tiers* tiersNew(void)
|
|
{
|
|
return tr_new0(tr_torrent_tiers, 1);
|
|
}
|
|
|
|
static void tiersDestruct(tr_torrent_tiers* tt)
|
|
{
|
|
for (int i = 0; i < tt->tracker_count; ++i)
|
|
{
|
|
trackerDestruct(&tt->trackers[i]);
|
|
}
|
|
|
|
tr_free(tt->trackers);
|
|
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
tierDestruct(&tt->tiers[i]);
|
|
}
|
|
|
|
tr_free(tt->tiers);
|
|
}
|
|
|
|
static void tiersFree(tr_torrent_tiers* tt)
|
|
{
|
|
tiersDestruct(tt);
|
|
tr_free(tt);
|
|
}
|
|
|
|
static tr_tier* getTier(tr_announcer* announcer, uint8_t const* info_hash, int tierId)
|
|
{
|
|
tr_tier* tier = nullptr;
|
|
|
|
if (announcer != nullptr)
|
|
{
|
|
tr_session* session = announcer->session;
|
|
tr_torrent* tor = tr_torrentFindFromHash(session, info_hash);
|
|
|
|
if (tor != nullptr && tor->tiers != nullptr)
|
|
{
|
|
tr_torrent_tiers* tt = tor->tiers;
|
|
|
|
for (int i = 0; tier == nullptr && i < tt->tier_count; ++i)
|
|
{
|
|
if (tt->tiers[i].key == tierId)
|
|
{
|
|
tier = &tt->tiers[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return tier;
|
|
}
|
|
|
|
/***
|
|
**** PUBLISH
|
|
***/
|
|
|
|
static void publishMessage(tr_tier* tier, char const* msg, TrackerEventType type)
|
|
{
|
|
if (tier != nullptr && tier->tor != nullptr && tier->tor->tiers != nullptr && tier->tor->tiers->callback != nullptr)
|
|
{
|
|
tr_torrent_tiers* tiers = tier->tor->tiers;
|
|
auto event = tr_tracker_event{};
|
|
event.messageType = type;
|
|
event.text = msg;
|
|
|
|
if (tier->currentTracker != nullptr)
|
|
{
|
|
event.tracker = tier->currentTracker->announce;
|
|
}
|
|
|
|
(*tiers->callback)(tier->tor, &event, tiers->callbackData);
|
|
}
|
|
}
|
|
|
|
static void publishErrorClear(tr_tier* tier)
|
|
{
|
|
publishMessage(tier, nullptr, TR_TRACKER_ERROR_CLEAR);
|
|
}
|
|
|
|
static void publishWarning(tr_tier* tier, char const* msg)
|
|
{
|
|
publishMessage(tier, msg, TR_TRACKER_WARNING);
|
|
}
|
|
|
|
static void publishError(tr_tier* tier, char const* msg)
|
|
{
|
|
publishMessage(tier, msg, TR_TRACKER_ERROR);
|
|
}
|
|
|
|
static void publishPeerCounts(tr_tier* tier, int seeders, int leechers)
|
|
{
|
|
if (tier->tor->tiers->callback != nullptr)
|
|
{
|
|
auto e = tr_tracker_event{};
|
|
e.messageType = TR_TRACKER_COUNTS;
|
|
e.seeders = seeders;
|
|
e.leechers = leechers;
|
|
dbgmsg(tier, "peer counts: %d seeders, %d leechers.", seeders, leechers);
|
|
|
|
(*tier->tor->tiers->callback)(tier->tor, &e, nullptr);
|
|
}
|
|
}
|
|
|
|
static void publishPeersPex(tr_tier* tier, int seeders, int leechers, tr_pex const* pex, int n)
|
|
{
|
|
if (tier->tor->tiers->callback != nullptr)
|
|
{
|
|
auto e = tr_tracker_event{};
|
|
e.messageType = TR_TRACKER_PEERS;
|
|
e.seeders = seeders;
|
|
e.leechers = leechers;
|
|
e.pex = pex;
|
|
e.pexCount = n;
|
|
dbgmsg(tier, "tracker knows of %d seeders and %d leechers and gave a list of %d peers.", seeders, leechers, n);
|
|
|
|
(*tier->tor->tiers->callback)(tier->tor, &e, nullptr);
|
|
}
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
struct ann_tracker_info
|
|
{
|
|
tr_tracker_info info;
|
|
|
|
char* scheme;
|
|
char* host;
|
|
char* path;
|
|
int port;
|
|
};
|
|
|
|
/* primary key: tier
|
|
* secondary key: udp comes before http */
|
|
static int filter_trackers_compare_func(void const* va, void const* vb)
|
|
{
|
|
auto* a = static_cast<struct ann_tracker_info const*>(va);
|
|
auto* b = static_cast<struct ann_tracker_info const*>(vb);
|
|
|
|
if (a->info.tier != b->info.tier)
|
|
{
|
|
return a->info.tier - b->info.tier;
|
|
}
|
|
|
|
return -strcmp(a->scheme, b->scheme);
|
|
}
|
|
|
|
/**
|
|
* Massages the incoming list of trackers into something we can use.
|
|
*/
|
|
static tr_tracker_info* filter_trackers(tr_tracker_info const* input, int input_count, int* setme_count)
|
|
{
|
|
int n = 0;
|
|
struct ann_tracker_info* tmp = tr_new0(struct ann_tracker_info, input_count);
|
|
|
|
/* build a list of valid trackers */
|
|
for (int i = 0; i < input_count; ++i)
|
|
{
|
|
if (tr_urlIsValidTracker(input[i].announce))
|
|
{
|
|
int port = 0;
|
|
char* scheme = nullptr;
|
|
char* host = nullptr;
|
|
char* path = nullptr;
|
|
tr_urlParse(input[i].announce, TR_BAD_SIZE, &scheme, &host, &port, &path);
|
|
|
|
/* weed out one common source of duplicates:
|
|
* "http://tracker/announce" +
|
|
* "http://tracker:80/announce"
|
|
*/
|
|
bool is_duplicate = false;
|
|
for (int j = 0; !is_duplicate && j < n; ++j)
|
|
{
|
|
is_duplicate = tmp[j].port == port && strcmp(tmp[j].scheme, scheme) == 0 && strcmp(tmp[j].host, host) == 0 &&
|
|
strcmp(tmp[j].path, path) == 0;
|
|
}
|
|
|
|
if (is_duplicate)
|
|
{
|
|
tr_free(path);
|
|
tr_free(host);
|
|
tr_free(scheme);
|
|
continue;
|
|
}
|
|
|
|
tmp[n].info = input[i];
|
|
tmp[n].scheme = scheme;
|
|
tmp[n].host = host;
|
|
tmp[n].port = port;
|
|
tmp[n].path = path;
|
|
n++;
|
|
}
|
|
}
|
|
|
|
/* if two announce URLs differ only by scheme, put them in the same tier.
|
|
* (note: this can leave gaps in the `tier' values, but since the calling
|
|
* function doesn't care, there's no point in removing the gaps...) */
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
for (int j = i + 1; j < n; ++j)
|
|
{
|
|
if (tmp[i].info.tier != tmp[j].info.tier && tmp[i].port == tmp[j].port &&
|
|
tr_strcmp0(tmp[i].host, tmp[j].host) == 0 && tr_strcmp0(tmp[i].path, tmp[j].path) == 0)
|
|
{
|
|
tmp[j].info.tier = tmp[i].info.tier;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* sort them, for two reasons:
|
|
* (1) unjumble the tiers from the previous step
|
|
* (2) move the UDP trackers to the front of each tier */
|
|
qsort(tmp, n, sizeof(struct ann_tracker_info), filter_trackers_compare_func);
|
|
|
|
/* build the output */
|
|
*setme_count = n;
|
|
struct tr_tracker_info* const ret = tr_new0(tr_tracker_info, n);
|
|
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
ret[i] = tmp[i].info;
|
|
}
|
|
|
|
/* cleanup */
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
tr_free(tmp[i].path);
|
|
tr_free(tmp[i].host);
|
|
tr_free(tmp[i].scheme);
|
|
}
|
|
|
|
tr_free(tmp);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void addTorrentToTier(tr_torrent_tiers* tt, tr_torrent* tor)
|
|
{
|
|
auto n = int{};
|
|
tr_tracker_info* infos = filter_trackers(tor->info.trackers, tor->info.trackerCount, &n);
|
|
|
|
/* build the array of trackers */
|
|
tt->trackers = tr_new0(tr_tracker, n);
|
|
tt->tracker_count = n;
|
|
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
trackerConstruct(tor->session->announcer, &tt->trackers[i], &infos[i]);
|
|
}
|
|
|
|
/* count how many tiers there are */
|
|
auto tier_count = int{};
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
if (i == 0 || infos[i].tier != infos[i - 1].tier)
|
|
{
|
|
++tier_count;
|
|
}
|
|
}
|
|
|
|
/* build the array of tiers */
|
|
tr_tier* tier = nullptr;
|
|
tt->tiers = tr_new0(tr_tier, tier_count);
|
|
tt->tier_count = 0;
|
|
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
if (i != 0 && infos[i].tier == infos[i - 1].tier)
|
|
{
|
|
++tier->tracker_count;
|
|
}
|
|
else
|
|
{
|
|
tier = &tt->tiers[tt->tier_count++];
|
|
tierConstruct(tier, tor);
|
|
tier->trackers = &tt->trackers[i];
|
|
tier->tracker_count = 1;
|
|
tierIncrementTracker(tier);
|
|
}
|
|
}
|
|
|
|
/* cleanup */
|
|
tr_free(infos);
|
|
}
|
|
|
|
tr_torrent_tiers* tr_announcerAddTorrent(tr_torrent* tor, tr_tracker_callback callback, void* callbackData)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
|
|
tr_torrent_tiers* tiers = tiersNew();
|
|
tiers->callback = callback;
|
|
tiers->callbackData = callbackData;
|
|
|
|
addTorrentToTier(tiers, tor);
|
|
|
|
return tiers;
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
static bool tierCanManualAnnounce(tr_tier const* tier)
|
|
{
|
|
return tier->manualAnnounceAllowedAt <= tr_time();
|
|
}
|
|
|
|
bool tr_announcerCanManualAnnounce(tr_torrent const* tor)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(tor->tiers != nullptr);
|
|
|
|
struct tr_torrent_tiers const* tt = nullptr;
|
|
|
|
if (tor->isRunning)
|
|
{
|
|
tt = tor->tiers;
|
|
}
|
|
|
|
/* return true if any tier can manual announce */
|
|
for (int i = 0; tt != nullptr && i < tt->tier_count; ++i)
|
|
{
|
|
if (tierCanManualAnnounce(&tt->tiers[i]))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
time_t tr_announcerNextManualAnnounce(tr_torrent const* tor)
|
|
{
|
|
time_t ret = ~(time_t)0;
|
|
struct tr_torrent_tiers const* tt = tor->tiers;
|
|
|
|
/* find the earliest manual announce time from all peers */
|
|
for (int i = 0; tt != nullptr && i < tt->tier_count; ++i)
|
|
{
|
|
if (tt->tiers[i].isRunning)
|
|
{
|
|
ret = std::min(ret, tt->tiers[i].manualAnnounceAllowedAt);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void dbgmsg_tier_announce_queue(tr_tier const* tier)
|
|
{
|
|
if (tr_logGetDeepEnabled())
|
|
{
|
|
char name[128];
|
|
struct evbuffer* buf = evbuffer_new();
|
|
|
|
tier_build_log_name(tier, name, sizeof(name));
|
|
|
|
for (int i = 0; i < tier->announce_event_count; ++i)
|
|
{
|
|
tr_announce_event const e = tier->announce_events[i];
|
|
char const* str = tr_announce_event_get_string(e);
|
|
evbuffer_add_printf(buf, "[%d:%s]", i, str);
|
|
}
|
|
|
|
char* const message = evbuffer_free_to_str(buf, nullptr);
|
|
tr_logAddDeep(__FILE__, __LINE__, name, "announce queue is %s", message);
|
|
tr_free(message);
|
|
}
|
|
}
|
|
|
|
// higher priorities go to the front of the announce queue
|
|
static void tier_update_announce_priority(tr_tier* tier)
|
|
{
|
|
int priority = -1;
|
|
|
|
for (int i = 0; i < tier->announce_event_count; ++i)
|
|
{
|
|
priority = std::max(priority, int{ tier->announce_events[i] });
|
|
}
|
|
|
|
tier->announce_event_priority = priority;
|
|
}
|
|
|
|
static void tier_announce_remove_trailing(tr_tier* tier, tr_announce_event e)
|
|
{
|
|
while (tier->announce_event_count > 0 && tier->announce_events[tier->announce_event_count - 1] == e)
|
|
{
|
|
--tier->announce_event_count;
|
|
}
|
|
|
|
tier_update_announce_priority(tier);
|
|
}
|
|
|
|
static void tier_announce_event_push(tr_tier* tier, tr_announce_event e, time_t announceAt)
|
|
{
|
|
TR_ASSERT(tier != nullptr);
|
|
|
|
dbgmsg_tier_announce_queue(tier);
|
|
dbgmsg(tier, "queued \"%s\"", tr_announce_event_get_string(e));
|
|
|
|
if (tier->announce_event_count > 0)
|
|
{
|
|
/* 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 has_completed = false;
|
|
tr_announce_event const c = TR_ANNOUNCE_EVENT_COMPLETED;
|
|
|
|
for (int i = 0; !has_completed && i < tier->announce_event_count; ++i)
|
|
{
|
|
has_completed = c == tier->announce_events[i];
|
|
}
|
|
|
|
tier->announce_event_count = 0;
|
|
|
|
if (has_completed)
|
|
{
|
|
tier->announce_events[tier->announce_event_count++] = c;
|
|
tier_update_announce_priority(tier);
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* make room in the array for another event */
|
|
if (tier->announce_event_alloc <= tier->announce_event_count)
|
|
{
|
|
tier->announce_event_alloc += 4;
|
|
tier->announce_events = tr_renew(tr_announce_event, tier->announce_events, tier->announce_event_alloc);
|
|
}
|
|
|
|
/* add it */
|
|
tier->announceAt = announceAt;
|
|
tier->announce_events[tier->announce_event_count++] = e;
|
|
tier_update_announce_priority(tier);
|
|
|
|
dbgmsg_tier_announce_queue(tier);
|
|
dbgmsg(tier, "announcing in %d seconds", (int)difftime(announceAt, tr_time()));
|
|
}
|
|
|
|
static tr_announce_event tier_announce_event_pull(tr_tier* tier)
|
|
{
|
|
tr_announce_event const e = tier->announce_events[0];
|
|
|
|
tr_removeElementFromArray(tier->announce_events, 0, sizeof(tr_announce_event), tier->announce_event_count);
|
|
--tier->announce_event_count;
|
|
tier_update_announce_priority(tier);
|
|
|
|
return e;
|
|
}
|
|
|
|
static void torrentAddAnnounce(tr_torrent* tor, tr_announce_event e, time_t announceAt)
|
|
{
|
|
struct tr_torrent_tiers* tt = tor->tiers;
|
|
|
|
/* walk through each tier and tell them to announce */
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
tier_announce_event_push(&tt->tiers[i], e, announceAt);
|
|
}
|
|
}
|
|
|
|
void tr_announcerTorrentStarted(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_announcerTorrentStopped(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)
|
|
{
|
|
tr_announcerTorrentStarted(tor);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
void tr_announcerAddBytes(tr_torrent* tor, int type, uint32_t byteCount)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(tor));
|
|
TR_ASSERT(type == TR_ANN_UP || type == TR_ANN_DOWN || type == TR_ANN_CORRUPT);
|
|
|
|
struct tr_torrent_tiers* tt = tor->tiers;
|
|
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
tt->tiers[i].byteCounts[type] += byteCount;
|
|
}
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
static tr_announce_request* announce_request_new(
|
|
tr_announcer const* announcer,
|
|
tr_torrent* tor,
|
|
tr_tier const* tier,
|
|
tr_announce_event event)
|
|
{
|
|
tr_announce_request* req = tr_new0(tr_announce_request, 1);
|
|
req->port = tr_sessionGetPublicPeerPort(announcer->session);
|
|
req->url = tr_strdup(tier->currentTracker->announce);
|
|
req->tracker_id_str = tr_strdup(tier->currentTracker->tracker_id_str);
|
|
memcpy(req->info_hash, tor->info.hash, SHA_DIGEST_LENGTH);
|
|
req->peer_id = tr_torrentGetPeerId(tor);
|
|
req->up = tier->byteCounts[TR_ANN_UP];
|
|
req->down = tier->byteCounts[TR_ANN_DOWN];
|
|
req->corrupt = tier->byteCounts[TR_ANN_CORRUPT];
|
|
req->leftUntilComplete = tr_torrentHasMetadata(tor) ? tor->info.totalSize - tr_torrentHaveTotal(tor) : INT64_MAX;
|
|
req->event = event;
|
|
req->numwant = event == TR_ANNOUNCE_EVENT_STOPPED ? 0 : Numwant;
|
|
req->key = announcer->key;
|
|
req->partial_seed = tr_torrentGetCompleteness(tor) == TR_PARTIAL_SEED;
|
|
tier_build_log_name(tier, req->log_name, sizeof(req->log_name));
|
|
return req;
|
|
}
|
|
|
|
static void announce_request_free(tr_announce_request* req);
|
|
|
|
void tr_announcerRemoveTorrent(tr_announcer* announcer, tr_torrent* tor)
|
|
{
|
|
struct tr_torrent_tiers const* tt = tor->tiers;
|
|
|
|
if (tt != nullptr)
|
|
{
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
tr_tier const* tier = &tt->tiers[i];
|
|
|
|
if (tier->isRunning)
|
|
{
|
|
tr_announce_event const e = TR_ANNOUNCE_EVENT_STOPPED;
|
|
tr_announce_request* req = announce_request_new(announcer, tor, tier, e);
|
|
|
|
if (announcer->stops.count(req))
|
|
{
|
|
announce_request_free(req);
|
|
}
|
|
else
|
|
{
|
|
announcer->stops.insert(req);
|
|
}
|
|
}
|
|
}
|
|
|
|
tiersFree(tor->tiers);
|
|
tor->tiers = nullptr;
|
|
}
|
|
}
|
|
|
|
static int getRetryInterval(tr_tracker const* t)
|
|
{
|
|
switch (t->consecutiveFailures)
|
|
{
|
|
case 0:
|
|
return 0;
|
|
|
|
case 1:
|
|
return 20;
|
|
|
|
case 2:
|
|
return tr_rand_int_weak(60) + 60 * 5;
|
|
|
|
case 3:
|
|
return tr_rand_int_weak(60) + 60 * 15;
|
|
|
|
case 4:
|
|
return tr_rand_int_weak(60) + 60 * 30;
|
|
|
|
case 5:
|
|
return tr_rand_int_weak(60) + 60 * 60;
|
|
|
|
default:
|
|
return tr_rand_int_weak(60) + 60 * 120;
|
|
}
|
|
}
|
|
|
|
struct announce_data
|
|
{
|
|
int tierId;
|
|
time_t timeSent;
|
|
tr_announce_event event;
|
|
tr_session* session;
|
|
|
|
/** If the request succeeds, the value for tier's "isRunning" flag */
|
|
bool isRunningOnSuccess;
|
|
};
|
|
|
|
static void on_announce_error(tr_tier* tier, char const* err, tr_announce_event e)
|
|
{
|
|
/* increment the error count */
|
|
if (tier->currentTracker != nullptr)
|
|
{
|
|
++tier->currentTracker->consecutiveFailures;
|
|
}
|
|
|
|
/* set the error message */
|
|
dbgmsg(tier, "%s", err);
|
|
tr_logAddTorInfo(tier->tor, "%s", err);
|
|
tr_strlcpy(tier->lastAnnounceStr, err, sizeof(tier->lastAnnounceStr));
|
|
|
|
/* switch to the next tracker */
|
|
tierIncrementTracker(tier);
|
|
|
|
/* schedule a reannounce */
|
|
int const interval = getRetryInterval(tier->currentTracker);
|
|
dbgmsg(tier, "Retrying announce in %d seconds.", interval);
|
|
tr_logAddTorInfo(tier->tor, "Retrying announce in %d seconds.", interval);
|
|
tier_announce_event_push(tier, e, tr_time() + interval);
|
|
}
|
|
|
|
static void on_announce_done(tr_announce_response const* response, void* vdata)
|
|
{
|
|
auto* data = static_cast<struct announce_data*>(vdata);
|
|
tr_announcer* announcer = data->session->announcer;
|
|
tr_tier* tier = getTier(announcer, response->info_hash, data->tierId);
|
|
time_t const now = tr_time();
|
|
tr_announce_event const event = data->event;
|
|
|
|
if (tier != nullptr)
|
|
{
|
|
dbgmsg(
|
|
tier,
|
|
"Got announce response: "
|
|
"connected:%d "
|
|
"timeout:%d "
|
|
"seeders:%d "
|
|
"leechers:%d "
|
|
"downloads:%d "
|
|
"interval:%d "
|
|
"min_interval:%d "
|
|
"tracker_id_str:%s "
|
|
"pex:%zu "
|
|
"pex6:%zu "
|
|
"err:%s "
|
|
"warn:%s",
|
|
(int)response->did_connect,
|
|
(int)response->did_timeout,
|
|
response->seeders,
|
|
response->leechers,
|
|
response->downloads,
|
|
response->interval,
|
|
response->min_interval,
|
|
response->tracker_id_str != nullptr ? response->tracker_id_str : "none",
|
|
response->pex_count,
|
|
response->pex6_count,
|
|
response->errmsg != nullptr ? response->errmsg : "none",
|
|
response->warning != nullptr ? response->warning : "none");
|
|
|
|
tier->lastAnnounceTime = now;
|
|
tier->lastAnnounceTimedOut = response->did_timeout;
|
|
tier->lastAnnounceSucceeded = false;
|
|
tier->isAnnouncing = false;
|
|
tier->manualAnnounceAllowedAt = now + tier->announceMinIntervalSec;
|
|
|
|
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 (response->errmsg != nullptr)
|
|
{
|
|
/* 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 (tier->tor->info.trackerCount < 2)
|
|
{
|
|
publishError(tier, response->errmsg);
|
|
}
|
|
|
|
on_announce_error(tier, response->errmsg, event);
|
|
}
|
|
else
|
|
{
|
|
auto const isStopped = event == TR_ANNOUNCE_EVENT_STOPPED;
|
|
auto leechers = int{};
|
|
auto scrape_fields = int{};
|
|
auto seeders = int{};
|
|
|
|
publishErrorClear(tier);
|
|
|
|
tr_tracker* const tracker = tier->currentTracker;
|
|
if (tracker != nullptr)
|
|
{
|
|
tracker->consecutiveFailures = 0;
|
|
|
|
if (response->seeders >= 0)
|
|
{
|
|
tracker->seederCount = seeders = response->seeders;
|
|
++scrape_fields;
|
|
}
|
|
|
|
if (response->leechers >= 0)
|
|
{
|
|
tracker->leecherCount = leechers = response->leechers;
|
|
++scrape_fields;
|
|
}
|
|
|
|
if (response->downloads >= 0)
|
|
{
|
|
tracker->downloadCount = response->downloads;
|
|
++scrape_fields;
|
|
}
|
|
|
|
if (response->tracker_id_str != nullptr)
|
|
{
|
|
tr_free(tracker->tracker_id_str);
|
|
tracker->tracker_id_str = tr_strdup(response->tracker_id_str);
|
|
}
|
|
}
|
|
|
|
char const* const str = response->warning;
|
|
if (str != nullptr)
|
|
{
|
|
tr_strlcpy(tier->lastAnnounceStr, str, sizeof(tier->lastAnnounceStr));
|
|
dbgmsg(tier, "tracker gave \"%s\"", str);
|
|
publishWarning(tier, str);
|
|
}
|
|
else
|
|
{
|
|
tr_strlcpy(tier->lastAnnounceStr, _("Success"), sizeof(tier->lastAnnounceStr));
|
|
}
|
|
|
|
if (response->min_interval != 0)
|
|
{
|
|
tier->announceMinIntervalSec = response->min_interval;
|
|
}
|
|
|
|
if (response->interval != 0)
|
|
{
|
|
tier->announceIntervalSec = response->interval;
|
|
}
|
|
|
|
if (response->pex_count > 0)
|
|
{
|
|
publishPeersPex(tier, seeders, leechers, response->pex, response->pex_count);
|
|
}
|
|
|
|
if (response->pex6_count > 0)
|
|
{
|
|
publishPeersPex(tier, seeders, leechers, response->pex6, response->pex6_count);
|
|
}
|
|
|
|
publishPeerCounts(tier, seeders, leechers);
|
|
|
|
tier->isRunning = data->isRunningOnSuccess;
|
|
|
|
/* if the tracker included scrape fields in its announce response,
|
|
then a separate scrape isn't needed */
|
|
if (scrape_fields >= 3 || (scrape_fields >= 1 && tracker->scrape_info == nullptr))
|
|
{
|
|
tr_logAddTorDbg(
|
|
tier->tor,
|
|
"Announce response contained scrape info; "
|
|
"rescheduling next scrape to %d seconds from now.",
|
|
tier->scrapeIntervalSec);
|
|
tier->scrapeAt = get_next_scrape_time(announcer->session, tier, tier->scrapeIntervalSec);
|
|
tier->lastScrapeTime = now;
|
|
tier->lastScrapeSucceeded = true;
|
|
}
|
|
else if (tier->lastScrapeTime + tier->scrapeIntervalSec <= now)
|
|
{
|
|
tier->scrapeAt = get_next_scrape_time(announcer->session, tier, 0);
|
|
}
|
|
|
|
tier->lastAnnounceSucceeded = true;
|
|
tier->lastAnnouncePeerCount = response->pex_count + response->pex6_count;
|
|
|
|
if (isStopped)
|
|
{
|
|
/* 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 (!isStopped && tier->announce_event_count == 0)
|
|
{
|
|
/* the queue is empty, so enqueue a perodic update */
|
|
int const i = tier->announceIntervalSec;
|
|
dbgmsg(tier, "Sending periodic reannounce in %d seconds", i);
|
|
tier_announce_event_push(tier, TR_ANNOUNCE_EVENT_NONE, now + i);
|
|
}
|
|
}
|
|
}
|
|
|
|
tr_free(data);
|
|
}
|
|
|
|
static void announce_request_free(tr_announce_request* req)
|
|
{
|
|
tr_free(req->tracker_id_str);
|
|
tr_free(req->url);
|
|
tr_free(req);
|
|
}
|
|
|
|
static void announce_request_delegate(
|
|
tr_announcer* announcer,
|
|
tr_announce_request* request,
|
|
tr_announce_response_func callback,
|
|
void* callback_data)
|
|
{
|
|
tr_session* session = announcer->session;
|
|
|
|
#if 0
|
|
|
|
fprintf(stderr, "ANNOUNCE: event %s isPartialSeed %d port %d key %d numwant %d up %" PRIu64 " down %" PRIu64
|
|
" corrupt %" PRIu64 " left %" PRIu64 " url [%s] tracker_id_str [%s] peer_id [%20.20s]\n",
|
|
tr_announce_event_get_string(request->event), (int)request->partial_seed, (int)request->port, request->key,
|
|
request->numwant, request->up, request->down, request->corrupt, request->leftUntilComplete, request->url,
|
|
request->tracker_id_str, request->peer_id);
|
|
|
|
#endif
|
|
|
|
if (strncmp(request->url, "http", 4) == 0)
|
|
{
|
|
tr_tracker_http_announce(session, request, callback, callback_data);
|
|
}
|
|
else if (strncmp(request->url, "udp://", 6) == 0)
|
|
{
|
|
tr_tracker_udp_announce(session, request, callback, callback_data);
|
|
}
|
|
else
|
|
{
|
|
tr_logAddError("Unsupported url: %s", request->url);
|
|
}
|
|
|
|
announce_request_free(request);
|
|
}
|
|
|
|
static void tierAnnounce(tr_announcer* announcer, tr_tier* tier)
|
|
{
|
|
TR_ASSERT(!tier->isAnnouncing);
|
|
TR_ASSERT(tier->announce_event_count > 0);
|
|
|
|
time_t const now = tr_time();
|
|
|
|
tr_torrent* tor = tier->tor;
|
|
tr_announce_event announce_event = tier_announce_event_pull(tier);
|
|
tr_announce_request* req = announce_request_new(announcer, tor, tier, announce_event);
|
|
|
|
struct announce_data* data = tr_new0(struct announce_data, 1);
|
|
data->session = announcer->session;
|
|
data->tierId = tier->key;
|
|
data->isRunningOnSuccess = tor->isRunning;
|
|
data->timeSent = now;
|
|
data->event = announce_event;
|
|
|
|
tier->isAnnouncing = true;
|
|
tier->lastAnnounceStartTime = now;
|
|
|
|
announce_request_delegate(announcer, req, on_announce_done, data);
|
|
}
|
|
|
|
/***
|
|
****
|
|
**** SCRAPE
|
|
****
|
|
***/
|
|
|
|
static constexpr 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 constexpr TooLongErrors = std::array<std::string_view, 3>{
|
|
"Bad Request",
|
|
"GET string too long",
|
|
"Request-URI Too Long",
|
|
};
|
|
|
|
for (auto const& tle : TooLongErrors)
|
|
{
|
|
if (errmsg.find(tle) != std::string_view::npos)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void on_scrape_error(tr_session const* session, tr_tier* tier, char const* errmsg)
|
|
{
|
|
/* increment the error count */
|
|
if (tier->currentTracker != nullptr)
|
|
{
|
|
++tier->currentTracker->consecutiveFailures;
|
|
}
|
|
|
|
/* set the error message */
|
|
dbgmsg(tier, "Scrape error: %s", errmsg);
|
|
tr_logAddTorInfo(tier->tor, "Scrape error: %s", errmsg);
|
|
tr_strlcpy(tier->lastScrapeStr, errmsg, sizeof(tier->lastScrapeStr));
|
|
|
|
/* switch to the next tracker */
|
|
tierIncrementTracker(tier);
|
|
|
|
/* schedule a rescrape */
|
|
int const interval = getRetryInterval(tier->currentTracker);
|
|
dbgmsg(tier, "Retrying scrape in %zu seconds.", (size_t)interval);
|
|
tr_logAddTorInfo(tier->tor, "Retrying scrape in %zu seconds.", (size_t)interval);
|
|
tier->lastScrapeSucceeded = false;
|
|
tier->scrapeAt = get_next_scrape_time(session, tier, interval);
|
|
}
|
|
|
|
static tr_tier* find_tier(tr_torrent* tor, std::string const& scrape)
|
|
{
|
|
struct tr_torrent_tiers* tt = tor->tiers;
|
|
|
|
for (int i = 0; tt != nullptr && i < tt->tier_count; ++i)
|
|
{
|
|
tr_tracker const* const tracker = tt->tiers[i].currentTracker;
|
|
|
|
if (tracker != nullptr && tracker->scrape_info != nullptr && tracker->scrape_info->url == scrape)
|
|
{
|
|
return &tt->tiers[i];
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static void on_scrape_done(tr_scrape_response const* response, void* vsession)
|
|
{
|
|
time_t const now = tr_time();
|
|
auto* session = static_cast<tr_session*>(vsession);
|
|
tr_announcer* announcer = session->announcer;
|
|
|
|
for (int i = 0; i < response->row_count; ++i)
|
|
{
|
|
struct tr_scrape_response_row const* row = &response->rows[i];
|
|
tr_torrent* tor = tr_torrentFindFromHash(session, row->info_hash);
|
|
|
|
if (tor != nullptr)
|
|
{
|
|
tr_tier* tier = find_tier(tor, response->url);
|
|
|
|
if (tier != nullptr)
|
|
{
|
|
dbgmsg(
|
|
tier,
|
|
"scraped url:%s -- "
|
|
"did_connect:%d "
|
|
"did_timeout:%d "
|
|
"seeders:%d "
|
|
"leechers:%d "
|
|
"downloads:%d "
|
|
"downloaders:%d "
|
|
"min_request_interval:%d "
|
|
"err:%s ",
|
|
response->url.c_str(),
|
|
(int)response->did_connect,
|
|
(int)response->did_timeout,
|
|
row->seeders,
|
|
row->leechers,
|
|
row->downloads,
|
|
row->downloaders,
|
|
response->min_request_interval,
|
|
std::empty(response->errmsg) ? "none" : response->errmsg.c_str());
|
|
|
|
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->scrapeAt = get_next_scrape_time(session, tier, tier->scrapeIntervalSec);
|
|
tr_logAddTorDbg(tier->tor, "Scrape successful. Rescraping in %d seconds.", tier->scrapeIntervalSec);
|
|
|
|
tr_tracker* const tracker = tier->currentTracker;
|
|
if (tracker != nullptr)
|
|
{
|
|
if (row->seeders >= 0)
|
|
{
|
|
tracker->seederCount = row->seeders;
|
|
}
|
|
|
|
if (row->leechers >= 0)
|
|
{
|
|
tracker->leecherCount = row->leechers;
|
|
}
|
|
|
|
if (row->downloads >= 0)
|
|
{
|
|
tracker->downloadCount = row->downloads;
|
|
}
|
|
|
|
tracker->downloaderCount = row->downloaders;
|
|
tracker->consecutiveFailures = 0;
|
|
}
|
|
|
|
if (row->seeders >= 0 && row->leechers >= 0 && row->downloads >= 0)
|
|
{
|
|
publishPeerCounts(tier, row->seeders, row->leechers);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Maybe reduce the number of torrents in a multiscrape req */
|
|
if (multiscrape_too_big(response->errmsg))
|
|
{
|
|
auto const& url = response->url;
|
|
struct tr_scrape_info* const scrape_info = tr_announcerGetScrapeInfo(announcer, url);
|
|
if (scrape_info != nullptr)
|
|
{
|
|
int* multiscrape_max = &scrape_info->multiscrape_max;
|
|
|
|
/* Lower the max only if it hasn't already lowered for a similar error.
|
|
For example if N parallel multiscrapes all have the same `max` and
|
|
error out, lower the value once for that batch, not N times. */
|
|
if (*multiscrape_max >= response->row_count)
|
|
{
|
|
int const n = std::max(1, int{ *multiscrape_max - TrMultiscrapeStep });
|
|
if (*multiscrape_max != n)
|
|
{
|
|
char* scheme = nullptr;
|
|
char* host = nullptr;
|
|
auto port = int{};
|
|
if (tr_urlParse(std::data(url), std::size(url), &scheme, &host, &port, nullptr))
|
|
{
|
|
/* don't log the full URL, since that might have a personal announce id */
|
|
char* sanitized_url = tr_strdup_printf("%s://%s:%d", scheme, host, port);
|
|
tr_logAddNamedInfo(sanitized_url, "Reducing multiscrape max to %d", n);
|
|
tr_free(sanitized_url);
|
|
tr_free(host);
|
|
tr_free(scheme);
|
|
}
|
|
|
|
*multiscrape_max = n;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void scrape_request_delegate(
|
|
tr_announcer* announcer,
|
|
tr_scrape_request const* request,
|
|
tr_scrape_response_func callback,
|
|
void* callback_data)
|
|
{
|
|
tr_session* session = announcer->session;
|
|
|
|
if (strncmp(request->url, "http", 4) == 0)
|
|
{
|
|
tr_tracker_http_scrape(session, request, callback, callback_data);
|
|
}
|
|
else if (strncmp(request->url, "udp://", 6) == 0)
|
|
{
|
|
tr_tracker_udp_scrape(session, request, callback, callback_data);
|
|
}
|
|
else
|
|
{
|
|
tr_logAddError("Unsupported url: %s", request->url);
|
|
}
|
|
}
|
|
|
|
static void multiscrape(tr_announcer* announcer, std::vector<tr_tier*> const& tiers)
|
|
{
|
|
size_t request_count = 0;
|
|
time_t const now = tr_time();
|
|
tr_scrape_request requests[MaxScrapesPerUpkeep] = {};
|
|
|
|
/* batch as many info_hashes into a request as we can */
|
|
for (auto* tier : tiers)
|
|
{
|
|
struct tr_scrape_info* const scrape_info = tier->currentTracker->scrape_info;
|
|
uint8_t const* hash = tier->tor->info.hash;
|
|
bool found = false;
|
|
|
|
TR_ASSERT(scrape_info != nullptr);
|
|
|
|
/* 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)
|
|
{
|
|
tr_scrape_request* req = &requests[j];
|
|
|
|
if (req->info_hash_count >= scrape_info->multiscrape_max)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (scrape_info->url != req->url)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
memcpy(req->info_hash[req->info_hash_count++], hash, SHA_DIGEST_LENGTH);
|
|
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)
|
|
{
|
|
tr_scrape_request* req = &requests[request_count++];
|
|
req->url = scrape_info->url.c_str();
|
|
tier_build_log_name(tier, req->log_name, sizeof(req->log_name));
|
|
|
|
memcpy(req->info_hash[req->info_hash_count++], hash, SHA_DIGEST_LENGTH);
|
|
tier->isScraping = true;
|
|
tier->lastScrapeStartTime = now;
|
|
}
|
|
}
|
|
|
|
/* send the requests we just built */
|
|
for (size_t i = 0; i < request_count; ++i)
|
|
{
|
|
scrape_request_delegate(announcer, &requests[i], on_scrape_done, announcer->session);
|
|
}
|
|
}
|
|
|
|
static void flushCloseMessages(tr_announcer* announcer)
|
|
{
|
|
auto& stops = announcer->stops;
|
|
std::for_each(
|
|
std::begin(stops),
|
|
std::end(stops),
|
|
[&announcer](auto* stop) { announce_request_delegate(announcer, stop, nullptr, nullptr); });
|
|
stops.clear();
|
|
}
|
|
|
|
static constexpr bool tierNeedsToAnnounce(tr_tier const* tier, time_t const now)
|
|
{
|
|
return !tier->isAnnouncing && !tier->isScraping && tier->announceAt != 0 && tier->announceAt <= now &&
|
|
tier->announce_event_count > 0;
|
|
}
|
|
|
|
static constexpr bool tierNeedsToScrape(tr_tier const* tier, time_t const now)
|
|
{
|
|
return !tier->isScraping && tier->scrapeAt != 0 && tier->scrapeAt <= now && tier->currentTracker != nullptr &&
|
|
tier->currentTracker->scrape_info != nullptr;
|
|
}
|
|
|
|
static constexpr int countDownloaders(tr_tier const* tier)
|
|
{
|
|
tr_tracker const* const tracker = tier->currentTracker;
|
|
|
|
return tracker == nullptr ? 0 : tracker->downloaderCount + tracker->leecherCount;
|
|
}
|
|
|
|
static int compareAnnounceTiers(tr_tier const* a, tr_tier const* b)
|
|
{
|
|
/* prefer higher-priority events */
|
|
int const priority_a = a->announce_event_priority;
|
|
int const priority_b = b->announce_event_priority;
|
|
if (priority_a != priority_b)
|
|
{
|
|
return priority_a > priority_b ? -1 : 1;
|
|
}
|
|
|
|
/* prefer swarms where we might upload */
|
|
int const downloader_count_a = countDownloaders(a);
|
|
int const downloader_count_b = countDownloaders(b);
|
|
if (downloader_count_a != downloader_count_b)
|
|
{
|
|
return downloader_count_a > downloader_count_b ? -1 : 1;
|
|
}
|
|
|
|
/* prefer swarms where we might download */
|
|
bool const is_seed_a = tr_torrentIsSeed(a->tor);
|
|
bool const is_seed_b = tr_torrentIsSeed(b->tor);
|
|
if (is_seed_a != is_seed_b)
|
|
{
|
|
return is_seed_a ? 1 : -1;
|
|
}
|
|
|
|
/* prefer larger stats, to help ensure stats get recorded when stopping on shutdown */
|
|
auto const xa = a->byteCounts[TR_ANN_UP] + a->byteCounts[TR_ANN_DOWN];
|
|
auto const xb = b->byteCounts[TR_ANN_UP] + b->byteCounts[TR_ANN_DOWN];
|
|
if (xa != xb)
|
|
{
|
|
return xa > xb ? -1 : 1;
|
|
}
|
|
|
|
// announcements that have been waiting longer go first
|
|
if (a->announceAt != b->announceAt)
|
|
{
|
|
return a->announceAt < b->announceAt ? -1 : 1;
|
|
}
|
|
|
|
// the tiers are effectively equal priority, but add an arbitrary
|
|
// differentiation because ptrArray sorted mode hates equal items.
|
|
return a < b ? -1 : 1;
|
|
}
|
|
|
|
static void scrapeAndAnnounceMore(tr_announcer* announcer)
|
|
{
|
|
time_t const now = tr_time();
|
|
|
|
/* build a list of tiers that need to be announced */
|
|
auto announce_me = std::vector<tr_tier*>{};
|
|
auto scrape_me = std::vector<tr_tier*>{};
|
|
for (auto* tor : announcer->session->torrents)
|
|
{
|
|
struct tr_torrent_tiers* tt = tor->tiers;
|
|
|
|
for (int i = 0; tt != nullptr && i < tt->tier_count; ++i)
|
|
{
|
|
tr_tier* tier = &tt->tiers[i];
|
|
|
|
if (tierNeedsToAnnounce(tier, now))
|
|
{
|
|
announce_me.push_back(tier);
|
|
}
|
|
|
|
if (tierNeedsToScrape(tier, 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_logAddTorDbg(tier->tor, "%s", "Announcing to tracker");
|
|
tierAnnounce(announcer, tier);
|
|
}
|
|
}
|
|
|
|
static void onUpkeepTimer(evutil_socket_t /*fd*/, short /*what*/, void* vannouncer)
|
|
{
|
|
auto* announcer = static_cast<tr_announcer*>(vannouncer);
|
|
tr_session* session = announcer->session;
|
|
bool const is_closing = session->isClosed;
|
|
time_t const now = tr_time();
|
|
|
|
tr_sessionLock(session);
|
|
|
|
/* maybe send out some "stopped" messages for closed torrents */
|
|
flushCloseMessages(announcer);
|
|
|
|
/* maybe kick off some scrapes / announces whose time has come */
|
|
if (!is_closing)
|
|
{
|
|
scrapeAndAnnounceMore(announcer);
|
|
}
|
|
|
|
/* TAU upkeep */
|
|
if (announcer->tauUpkeepAt <= now)
|
|
{
|
|
announcer->tauUpkeepAt = now + TauUpkeepIntervalSecs;
|
|
tr_tracker_udp_upkeep(session);
|
|
}
|
|
|
|
/* set up the next timer */
|
|
tr_timerAddMsec(announcer->upkeepTimer, UpkeepIntervalMsec);
|
|
|
|
tr_sessionUnlock(session);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
tr_tracker_stat* tr_announcerStats(tr_torrent const* torrent, int* setmeTrackerCount)
|
|
{
|
|
TR_ASSERT(tr_isTorrent(torrent));
|
|
|
|
time_t const now = tr_time();
|
|
|
|
int out = 0;
|
|
struct tr_torrent_tiers const* const tt = torrent->tiers;
|
|
|
|
/* alloc the stats */
|
|
*setmeTrackerCount = tt->tracker_count;
|
|
tr_tracker_stat* const ret = tr_new0(tr_tracker_stat, tt->tracker_count);
|
|
|
|
/* populate the stats */
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
tr_tier const* const tier = &tt->tiers[i];
|
|
|
|
for (int j = 0; j < tier->tracker_count; ++j)
|
|
{
|
|
tr_tracker const* const tracker = &tier->trackers[j];
|
|
tr_tracker_stat* st = &ret[out++];
|
|
|
|
st->id = tracker->id;
|
|
tr_strlcpy(st->host, tracker->key, sizeof(st->host));
|
|
tr_strlcpy(st->announce, tracker->announce, sizeof(st->announce));
|
|
st->tier = i;
|
|
st->isBackup = tracker != tier->currentTracker;
|
|
st->lastScrapeStartTime = tier->lastScrapeStartTime;
|
|
|
|
if (tracker->scrape_info != nullptr)
|
|
{
|
|
tr_strlcpy(st->scrape, tracker->scrape_info->url.c_str(), sizeof(st->scrape));
|
|
}
|
|
else
|
|
{
|
|
st->scrape[0] = '\0';
|
|
}
|
|
|
|
st->seederCount = tracker->seederCount;
|
|
st->leecherCount = tracker->leecherCount;
|
|
st->downloadCount = tracker->downloadCount;
|
|
|
|
if (st->isBackup)
|
|
{
|
|
st->scrapeState = TR_TRACKER_INACTIVE;
|
|
st->announceState = TR_TRACKER_INACTIVE;
|
|
st->nextScrapeTime = 0;
|
|
st->nextAnnounceTime = 0;
|
|
}
|
|
else
|
|
{
|
|
if ((st->hasScraped = tier->lastScrapeTime != 0))
|
|
{
|
|
st->lastScrapeTime = tier->lastScrapeTime;
|
|
st->lastScrapeSucceeded = tier->lastScrapeSucceeded;
|
|
st->lastScrapeTimedOut = tier->lastScrapeTimedOut;
|
|
tr_strlcpy(st->lastScrapeResult, tier->lastScrapeStr, sizeof(st->lastScrapeResult));
|
|
}
|
|
|
|
if (tier->isScraping)
|
|
{
|
|
st->scrapeState = TR_TRACKER_ACTIVE;
|
|
}
|
|
else if (tier->scrapeAt == 0)
|
|
{
|
|
st->scrapeState = TR_TRACKER_INACTIVE;
|
|
}
|
|
else if (tier->scrapeAt > now)
|
|
{
|
|
st->scrapeState = TR_TRACKER_WAITING;
|
|
st->nextScrapeTime = tier->scrapeAt;
|
|
}
|
|
else
|
|
{
|
|
st->scrapeState = TR_TRACKER_QUEUED;
|
|
}
|
|
|
|
st->lastAnnounceStartTime = tier->lastAnnounceStartTime;
|
|
|
|
if ((st->hasAnnounced = tier->lastAnnounceTime != 0))
|
|
{
|
|
st->lastAnnounceTime = tier->lastAnnounceTime;
|
|
tr_strlcpy(st->lastAnnounceResult, tier->lastAnnounceStr, sizeof(st->lastAnnounceResult));
|
|
st->lastAnnounceSucceeded = tier->lastAnnounceSucceeded;
|
|
st->lastAnnounceTimedOut = tier->lastAnnounceTimedOut;
|
|
st->lastAnnouncePeerCount = tier->lastAnnouncePeerCount;
|
|
}
|
|
|
|
if (tier->isAnnouncing)
|
|
{
|
|
st->announceState = TR_TRACKER_ACTIVE;
|
|
}
|
|
else if (!torrent->isRunning || tier->announceAt == 0)
|
|
{
|
|
st->announceState = TR_TRACKER_INACTIVE;
|
|
}
|
|
else if (tier->announceAt > now)
|
|
{
|
|
st->announceState = TR_TRACKER_WAITING;
|
|
st->nextAnnounceTime = tier->announceAt;
|
|
}
|
|
else
|
|
{
|
|
st->announceState = TR_TRACKER_QUEUED;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void tr_announcerStatsFree(tr_tracker_stat* trackers, int /*trackerCount*/)
|
|
{
|
|
tr_free(trackers);
|
|
}
|
|
|
|
/***
|
|
****
|
|
***/
|
|
|
|
static void copy_tier_attributes_impl(struct tr_tier* tgt, int trackerIndex, tr_tier const* src)
|
|
{
|
|
/* sanity clause */
|
|
TR_ASSERT(trackerIndex < tgt->tracker_count);
|
|
TR_ASSERT(tr_strcmp0(tgt->trackers[trackerIndex].announce, src->currentTracker->announce) == 0);
|
|
|
|
tr_tier const keep = *tgt;
|
|
|
|
/* bitwise copy will handle most of tr_tier's fields... */
|
|
*tgt = *src;
|
|
|
|
/* ...fix the fields that can't be cleanly bitwise-copied */
|
|
tgt->wasCopied = true;
|
|
tgt->trackers = keep.trackers;
|
|
tgt->tracker_count = keep.tracker_count;
|
|
tgt->announce_events = static_cast<tr_announce_event*>(
|
|
tr_memdup(src->announce_events, sizeof(tr_announce_event) * src->announce_event_count));
|
|
tgt->announce_event_priority = src->announce_event_priority;
|
|
tgt->announce_event_count = src->announce_event_count;
|
|
tgt->announce_event_alloc = src->announce_event_count;
|
|
tgt->currentTrackerIndex = trackerIndex;
|
|
tgt->currentTracker = &tgt->trackers[trackerIndex];
|
|
tgt->currentTracker->seederCount = src->currentTracker->seederCount;
|
|
tgt->currentTracker->leecherCount = src->currentTracker->leecherCount;
|
|
tgt->currentTracker->downloadCount = src->currentTracker->downloadCount;
|
|
tgt->currentTracker->downloaderCount = src->currentTracker->downloaderCount;
|
|
}
|
|
|
|
static void copy_tier_attributes(struct tr_torrent_tiers* tt, tr_tier const* src)
|
|
{
|
|
bool found = false;
|
|
|
|
/* find a tier (if any) which has a match for src->currentTracker */
|
|
for (int i = 0; !found && i < tt->tier_count; ++i)
|
|
{
|
|
for (int j = 0; !found && j < tt->tiers[i].tracker_count; ++j)
|
|
{
|
|
if (tr_strcmp0(src->currentTracker->announce, tt->tiers[i].trackers[j].announce) == 0)
|
|
{
|
|
found = true;
|
|
copy_tier_attributes_impl(&tt->tiers[i], j, src);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void tr_announcerResetTorrent(tr_announcer* /*announcer*/, tr_torrent* tor)
|
|
{
|
|
TR_ASSERT(tor->tiers != nullptr);
|
|
|
|
time_t const now = tr_time();
|
|
|
|
struct tr_torrent_tiers* tt = tor->tiers;
|
|
tr_torrent_tiers old = *tt;
|
|
|
|
/* remove the old tiers / trackers */
|
|
tt->tiers = nullptr;
|
|
tt->trackers = nullptr;
|
|
tt->tier_count = 0;
|
|
tt->tracker_count = 0;
|
|
|
|
/* create the new tiers / trackers */
|
|
addTorrentToTier(tt, tor);
|
|
|
|
/* copy the old tiers' states into their replacements */
|
|
for (int i = 0; i < old.tier_count; ++i)
|
|
{
|
|
if (old.tiers[i].currentTracker != nullptr)
|
|
{
|
|
copy_tier_attributes(tt, &old.tiers[i]);
|
|
}
|
|
}
|
|
|
|
/* kickstart any tiers that didn't get started */
|
|
if (tor->isRunning)
|
|
{
|
|
for (int i = 0; i < tt->tier_count; ++i)
|
|
{
|
|
if (!tt->tiers[i].wasCopied)
|
|
{
|
|
tier_announce_event_push(&tt->tiers[i], TR_ANNOUNCE_EVENT_STARTED, now);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cleanup */
|
|
tiersDestruct(&old);
|
|
}
|