mirror of
https://github.com/transmission/transmission
synced 2024-12-21 23:32:35 +00:00
feat: dual stack udp tracker support (#6687)
* chore: housekeeping * refactor: reduce copying when building payloads * feat: dual-stack udp tracker support * refactor: convert function names to snake_case * fix: `readability-identifier-naming` warning * fix: account for dual-stack in tests * code review: add prefix to global names * fix: don't resolve to IPv4-mapped address * refactor: use `tr_address` method to check ip protocol * fix: workaround MSVC x86 build failure * fix: handle host components that has square brackets * Partial Revert: "fix: account for dual-stack in tests" Not needed anymore * fix: store ipv6 peers in pex6 --------- Co-authored-by: Charles Kerr <charles@charleskerr.com>
This commit is contained in:
parent
3677e7a591
commit
4657d210ba
11 changed files with 482 additions and 230 deletions
|
@ -157,10 +157,10 @@ struct tr_announce_response
|
|||
* This is only an upper bound: if the tracker complains about
|
||||
* length, announcer will incrementally lower the batch size.
|
||||
*/
|
||||
auto inline constexpr TR_MULTISCRAPE_MAX = 60;
|
||||
auto inline constexpr TrMultiscrapeMax = 60;
|
||||
|
||||
auto inline constexpr TR_ANNOUNCE_TIMEOUT_SEC = std::chrono::seconds{ 45 };
|
||||
auto inline constexpr TR_SCRAPE_TIMEOUT_SEC = std::chrono::seconds{ 30 };
|
||||
auto inline constexpr TrAnnounceTimeoutSec = std::chrono::seconds{ 45 };
|
||||
auto inline constexpr TrScrapeTimeoutSec = std::chrono::seconds{ 30 };
|
||||
|
||||
struct tr_scrape_request
|
||||
{
|
||||
|
@ -171,7 +171,7 @@ struct tr_scrape_request
|
|||
std::string log_name;
|
||||
|
||||
/* info hashes of the torrents to scrape */
|
||||
std::array<tr_sha1_digest_t, TR_MULTISCRAPE_MAX> info_hash;
|
||||
std::array<tr_sha1_digest_t, TrMultiscrapeMax> info_hash;
|
||||
|
||||
/* how many hashes to use in the info_hash field */
|
||||
int info_hash_count = 0;
|
||||
|
@ -209,7 +209,7 @@ struct tr_scrape_response
|
|||
int row_count;
|
||||
|
||||
/* the individual torrents' scrape results */
|
||||
std::array<tr_scrape_response_row, TR_MULTISCRAPE_MAX> rows;
|
||||
std::array<tr_scrape_response_row, TrMultiscrapeMax> rows;
|
||||
|
||||
/* the raw scrape url */
|
||||
tr_interned_string scrape_url;
|
||||
|
|
|
@ -265,7 +265,7 @@ void tr_tracker_http_announce(
|
|||
auto url = tr_urlbuf{};
|
||||
announce_url_new(url, session, request);
|
||||
auto options = tr_web::FetchOptions{ url.sv(), onAnnounceDone, d };
|
||||
options.timeout_secs = TR_ANNOUNCE_TIMEOUT_SEC;
|
||||
options.timeout_secs = TrAnnounceTimeoutSec;
|
||||
options.sndbuf = 4096;
|
||||
options.rcvbuf = 4096;
|
||||
|
||||
|
@ -542,7 +542,7 @@ void tr_tracker_http_scrape(tr_session const* session, tr_scrape_request const&
|
|||
scrape_url_new(scrape_url, request);
|
||||
tr_logAddTrace(fmt::format("Sending scrape to libcurl: '{}'", scrape_url), request.log_name);
|
||||
auto options = tr_web::FetchOptions{ scrape_url, onScrapeDone, d };
|
||||
options.timeout_secs = TR_SCRAPE_TIMEOUT_SEC;
|
||||
options.timeout_secs = TrScrapeTimeoutSec;
|
||||
options.sndbuf = 4096;
|
||||
options.rcvbuf = 4096;
|
||||
session->fetch(std::move(options));
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
#include <algorithm> // for std::find_if()
|
||||
#include <array>
|
||||
#include <chrono> // operator""ms, literals
|
||||
#include <climits> // CHAR_BIT
|
||||
#include <cstddef> // std::byte
|
||||
#include <cstdint> // uint32_t, uint64_t
|
||||
#include <cstring> // memcpy()
|
||||
|
@ -17,8 +16,8 @@
|
|||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <ws2tcpip.h>
|
||||
|
@ -55,13 +54,15 @@ namespace
|
|||
{
|
||||
using namespace std::literals;
|
||||
|
||||
// size defined by bep15
|
||||
// size defined by https://www.bittorrent.org/beps/bep_0015.html
|
||||
using tau_connection_t = uint64_t;
|
||||
using tau_transaction_t = uint32_t;
|
||||
|
||||
using InBuf = libtransmission::BufferReader<std::byte>;
|
||||
using PayloadBuffer = libtransmission::StackBuffer<4096, std::byte>;
|
||||
|
||||
using ipp_t = std::underlying_type_t<tr_address_type>;
|
||||
|
||||
constexpr auto TauConnectionTtlSecs = time_t{ 45 };
|
||||
|
||||
auto tau_transaction_new()
|
||||
|
@ -69,7 +70,8 @@ auto tau_transaction_new()
|
|||
return tr_rand_obj<tau_transaction_t>();
|
||||
}
|
||||
|
||||
// used in the "action" field of a request. Values defined in bep 15.
|
||||
// used in the "action" field of a request.
|
||||
// Values defined in https://www.bittorrent.org/beps/bep_0015.html
|
||||
enum tau_action_t : uint8_t
|
||||
{
|
||||
TAU_ACTION_CONNECT = 0,
|
||||
|
@ -85,22 +87,20 @@ struct tau_scrape_request
|
|||
tau_scrape_request(tr_scrape_request const& in, tr_scrape_response_func on_response)
|
||||
: on_response_{ std::move(on_response) }
|
||||
{
|
||||
this->response.scrape_url = in.scrape_url;
|
||||
this->response.row_count = in.info_hash_count;
|
||||
for (int i = 0; i < this->response.row_count; ++i)
|
||||
response.scrape_url = in.scrape_url;
|
||||
response.row_count = in.info_hash_count;
|
||||
for (int i = 0; i < response.row_count; ++i)
|
||||
{
|
||||
this->response.rows[i].info_hash = in.info_hash[i];
|
||||
response.rows[i].info_hash = in.info_hash[i];
|
||||
}
|
||||
|
||||
// build the payload
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint32(TAU_ACTION_SCRAPE);
|
||||
buf.add_uint32(transaction_id);
|
||||
payload.add_uint32(TAU_ACTION_SCRAPE);
|
||||
payload.add_uint32(transaction_id);
|
||||
for (int i = 0; i < in.info_hash_count; ++i)
|
||||
{
|
||||
buf.add(in.info_hash[i]);
|
||||
payload.add(in.info_hash[i]);
|
||||
}
|
||||
this->payload.insert(std::end(this->payload), std::begin(buf), std::end(buf));
|
||||
}
|
||||
|
||||
[[nodiscard]] auto has_callback() const noexcept
|
||||
|
@ -108,7 +108,7 @@ struct tau_scrape_request
|
|||
return !!on_response_;
|
||||
}
|
||||
|
||||
void requestFinished() const
|
||||
void request_finished() const
|
||||
{
|
||||
if (on_response_)
|
||||
{
|
||||
|
@ -121,10 +121,10 @@ struct tau_scrape_request
|
|||
response.did_connect = did_connect;
|
||||
response.did_timeout = did_timeout;
|
||||
response.errmsg = errmsg;
|
||||
requestFinished();
|
||||
request_finished();
|
||||
}
|
||||
|
||||
void onResponse(tau_action_t action, InBuf& buf)
|
||||
void on_response(tau_action_t action, InBuf& buf)
|
||||
{
|
||||
response.did_connect = true;
|
||||
response.did_timeout = false;
|
||||
|
@ -139,7 +139,7 @@ struct tau_scrape_request
|
|||
row.leechers = buf.to_uint32();
|
||||
}
|
||||
|
||||
requestFinished();
|
||||
request_finished();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -148,18 +148,20 @@ struct tau_scrape_request
|
|||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto expiresAt() const noexcept
|
||||
[[nodiscard]] constexpr auto expires_at() const noexcept
|
||||
{
|
||||
return created_at_ + TR_SCRAPE_TIMEOUT_SEC.count();
|
||||
return created_at_ + TrScrapeTimeoutSec.count();
|
||||
}
|
||||
|
||||
std::vector<std::byte> payload;
|
||||
PayloadBuffer payload;
|
||||
|
||||
time_t sent_at = 0;
|
||||
tau_transaction_t const transaction_id = tau_transaction_new();
|
||||
|
||||
tr_scrape_response response = {};
|
||||
|
||||
static auto constexpr ip_protocol = TR_AF_UNSPEC; // NOLINT readability-identifier-naming
|
||||
|
||||
private:
|
||||
time_t const created_at_ = tr_time();
|
||||
|
||||
|
@ -171,38 +173,39 @@ private:
|
|||
struct tau_announce_request
|
||||
{
|
||||
tau_announce_request(
|
||||
tr_address_type ip_protocol_in,
|
||||
std::optional<tr_address> announce_ip,
|
||||
tr_announce_request const& in,
|
||||
tr_announce_response_func on_response)
|
||||
: on_response_{ std::move(on_response) }
|
||||
: ip_protocol{ ip_protocol_in }
|
||||
, on_response_{ std::move(on_response) }
|
||||
{
|
||||
// https://www.bittorrent.org/beps/bep_0015.html sets key size at 32 bits
|
||||
static_assert(sizeof(tr_announce_request::key) * CHAR_BIT == 32);
|
||||
static_assert(sizeof(tr_announce_request::key) == sizeof(uint32_t));
|
||||
|
||||
response.info_hash = in.info_hash;
|
||||
|
||||
// build the payload
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint32(TAU_ACTION_ANNOUNCE);
|
||||
buf.add_uint32(transaction_id);
|
||||
buf.add(in.info_hash);
|
||||
buf.add(in.peer_id);
|
||||
buf.add_uint64(in.down);
|
||||
buf.add_uint64(in.leftUntilComplete);
|
||||
buf.add_uint64(in.up);
|
||||
buf.add_uint32(get_tau_announce_event(in.event));
|
||||
payload.add_uint32(TAU_ACTION_ANNOUNCE);
|
||||
payload.add_uint32(transaction_id);
|
||||
payload.add(in.info_hash);
|
||||
payload.add(in.peer_id);
|
||||
payload.add_uint64(in.down);
|
||||
payload.add_uint64(in.leftUntilComplete);
|
||||
payload.add_uint64(in.up);
|
||||
payload.add_uint32(get_tau_announce_event(in.event));
|
||||
if (announce_ip && announce_ip->is_ipv4())
|
||||
{
|
||||
buf.add_address(*announce_ip);
|
||||
// Since size of IP field is only 4 bytes long, we can only announce IPv4 addresses
|
||||
payload.add_address(*announce_ip);
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.add_uint32(0U);
|
||||
payload.add_uint32(0U);
|
||||
}
|
||||
buf.add_uint32(in.key);
|
||||
buf.add_uint32(in.numwant);
|
||||
buf.add_port(in.port);
|
||||
payload.insert(std::end(payload), std::begin(buf), std::end(buf));
|
||||
payload.add_uint32(in.key);
|
||||
payload.add_uint32(in.numwant);
|
||||
payload.add_port(in.port);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto has_callback() const noexcept
|
||||
|
@ -210,28 +213,28 @@ struct tau_announce_request
|
|||
return !!on_response_;
|
||||
}
|
||||
|
||||
void requestFinished() const
|
||||
void request_finished() const
|
||||
{
|
||||
if (on_response_)
|
||||
{
|
||||
on_response_(this->response);
|
||||
on_response_(response);
|
||||
}
|
||||
}
|
||||
|
||||
void fail(bool did_connect, bool did_timeout, std::string_view errmsg)
|
||||
{
|
||||
this->response.did_connect = did_connect;
|
||||
this->response.did_timeout = did_timeout;
|
||||
this->response.errmsg = errmsg;
|
||||
this->requestFinished();
|
||||
response.did_connect = did_connect;
|
||||
response.did_timeout = did_timeout;
|
||||
response.errmsg = errmsg;
|
||||
request_finished();
|
||||
}
|
||||
|
||||
void onResponse(tau_action_t action, InBuf& buf)
|
||||
void on_response(tr_address_type ip_protocol_resp, tau_action_t action, InBuf& buf)
|
||||
{
|
||||
auto const buflen = std::size(buf);
|
||||
|
||||
this->response.did_connect = true;
|
||||
this->response.did_timeout = false;
|
||||
response.did_connect = true;
|
||||
response.did_timeout = false;
|
||||
|
||||
if (action == TAU_ACTION_ANNOUNCE && buflen >= 3 * sizeof(uint32_t))
|
||||
{
|
||||
|
@ -239,8 +242,18 @@ struct tau_announce_request
|
|||
response.leechers = buf.to_uint32();
|
||||
response.seeders = buf.to_uint32();
|
||||
|
||||
response.pex = tr_pex::from_compact_ipv4(std::data(buf), std::size(buf), nullptr, 0);
|
||||
requestFinished();
|
||||
switch (ip_protocol_resp)
|
||||
{
|
||||
case TR_AF_INET:
|
||||
response.pex = tr_pex::from_compact_ipv4(std::data(buf), std::size(buf), nullptr, 0);
|
||||
break;
|
||||
case TR_AF_INET6:
|
||||
response.pex6 = tr_pex::from_compact_ipv6(std::data(buf), std::size(buf), nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
request_finished();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -249,23 +262,24 @@ struct tau_announce_request
|
|||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto expiresAt() const noexcept
|
||||
[[nodiscard]] constexpr auto expires_at() const noexcept
|
||||
{
|
||||
return created_at_ + TR_ANNOUNCE_TIMEOUT_SEC.count();
|
||||
return created_at_ + TrAnnounceTimeoutSec.count();
|
||||
}
|
||||
|
||||
enum tau_announce_event : uint8_t
|
||||
{
|
||||
// https://www.bittorrent.org/beps/bep_0015.html
|
||||
// Used in the "event" field of an announce request.
|
||||
// These values come from BEP 15
|
||||
TAU_ANNOUNCE_EVENT_NONE = 0,
|
||||
TAU_ANNOUNCE_EVENT_COMPLETED = 1,
|
||||
TAU_ANNOUNCE_EVENT_STARTED = 2,
|
||||
TAU_ANNOUNCE_EVENT_STOPPED = 3
|
||||
};
|
||||
|
||||
std::vector<std::byte> payload;
|
||||
PayloadBuffer payload;
|
||||
|
||||
tr_address_type const ip_protocol;
|
||||
time_t sent_at = 0;
|
||||
tau_transaction_t const transaction_id = tau_transaction_new();
|
||||
|
||||
|
@ -303,97 +317,144 @@ struct tau_tracker
|
|||
|
||||
tau_tracker(
|
||||
Mediator& mediator,
|
||||
std::string_view const interned_authority,
|
||||
std::string_view const interned_host,
|
||||
std::string_view const authority_in,
|
||||
std::string_view const host_in,
|
||||
std::string_view const host_lookup_in,
|
||||
tr_port const port_in)
|
||||
: authority{ interned_authority }
|
||||
, host{ interned_host }
|
||||
: authority{ authority_in }
|
||||
, host{ host_in }
|
||||
, host_lookup{ host_lookup_in }
|
||||
, port{ port_in }
|
||||
, mediator_{ mediator }
|
||||
{
|
||||
}
|
||||
|
||||
void sendto(std::byte const* buf, size_t buflen)
|
||||
void sendto(tr_address_type ip_protocol, std::byte const* buf, size_t buflen)
|
||||
{
|
||||
TR_ASSERT(addr_);
|
||||
if (!addr_)
|
||||
TR_ASSERT(tr_address::is_valid(ip_protocol));
|
||||
if (!tr_address::is_valid(ip_protocol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& [ss, sslen] = *addr_;
|
||||
TR_ASSERT(addr_[ip_protocol]);
|
||||
if (!addr_[ip_protocol])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& [ss, sslen] = *addr_[ip_protocol];
|
||||
mediator_.sendto(buf, buflen, reinterpret_cast<sockaddr const*>(&ss), sslen);
|
||||
}
|
||||
|
||||
void on_connection_response(tau_action_t action, InBuf& buf)
|
||||
void on_connection_response(tr_address_type ip_protocol, tau_action_t action, InBuf& buf)
|
||||
{
|
||||
this->connecting_at = 0;
|
||||
this->connection_transaction_id = 0;
|
||||
TR_ASSERT(tr_address::is_valid(ip_protocol));
|
||||
if (!tr_address::is_valid(ip_protocol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
connecting_at[ip_protocol] = 0;
|
||||
connection_transaction_id[ip_protocol] = 0;
|
||||
|
||||
if (action == TAU_ACTION_CONNECT)
|
||||
{
|
||||
this->connection_id = buf.to_uint64();
|
||||
this->connection_expiration_time = tr_time() + TauConnectionTtlSecs;
|
||||
logdbg(log_name(), fmt::format("Got a new connection ID from tracker: {}", this->connection_id));
|
||||
connection_id[ip_protocol] = buf.to_uint64();
|
||||
connection_expiration_time[ip_protocol] = tr_time() + TauConnectionTtlSecs;
|
||||
logdbg(
|
||||
log_name(),
|
||||
fmt::format(
|
||||
"Got a new {} connection ID from tracker: {}",
|
||||
tr_ip_protocol_to_sv(ip_protocol),
|
||||
connection_id[ip_protocol]));
|
||||
}
|
||||
else if (action == TAU_ACTION_ERROR)
|
||||
{
|
||||
std::string errmsg = !std::empty(buf) ? buf.to_string() : _("Connection failed");
|
||||
this->failAll(true, false, errmsg);
|
||||
std::string errmsg = !std::empty(buf) ?
|
||||
buf.to_string() :
|
||||
fmt::format(_("{ip_protocol} connection failed"), fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol)));
|
||||
fail_all(true, false, errmsg);
|
||||
logdbg(log_name(), std::move(errmsg));
|
||||
}
|
||||
|
||||
this->upkeep();
|
||||
upkeep();
|
||||
}
|
||||
|
||||
void upkeep(bool timeout_reqs = true)
|
||||
{
|
||||
time_t const now = tr_time();
|
||||
|
||||
// do we have a DNS request that's ready?
|
||||
if (addr_pending_dns_ && addr_pending_dns_->wait_for(0ms) == std::future_status::ready)
|
||||
for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
addr_ = addr_pending_dns_->get();
|
||||
addr_pending_dns_.reset();
|
||||
addr_expires_at_ = now + DnsRetryIntervalSecs;
|
||||
// do we have a DNS request that's ready?
|
||||
if (addr_pending_dns_[ipp] && addr_pending_dns_[ipp]->wait_for(0ms) == std::future_status::ready)
|
||||
{
|
||||
addr_[ipp] = addr_pending_dns_[ipp]->get();
|
||||
addr_pending_dns_[ipp].reset();
|
||||
addr_expires_at_[ipp] = now + DnsRetryIntervalSecs;
|
||||
}
|
||||
}
|
||||
|
||||
// are there any requests pending?
|
||||
if (this->is_idle())
|
||||
// are there any tracker requests pending?
|
||||
if (is_idle())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// update the addr if our lookup is past its shelf date
|
||||
if (!addr_pending_dns_ && addr_expires_at_ <= now)
|
||||
for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
// update the addr if our lookup is past its shelf date
|
||||
if (!addr_pending_dns_[ipp] && addr_expires_at_[ipp] <= now)
|
||||
{
|
||||
addr_[ipp].reset();
|
||||
addr_pending_dns_[ipp] = std::async(
|
||||
std::launch::async,
|
||||
[this](tr_address_type ip_protocol) { return lookup(ip_protocol); },
|
||||
static_cast<tr_address_type>(ipp));
|
||||
}
|
||||
}
|
||||
|
||||
// are there any dns requests pending?
|
||||
if (is_dns_pending())
|
||||
{
|
||||
addr_.reset();
|
||||
addr_pending_dns_ = std::async(std::launch::async, lookup, this->log_name(), this->host, this->port);
|
||||
return;
|
||||
}
|
||||
|
||||
logtrace(
|
||||
log_name(),
|
||||
fmt::format(
|
||||
"connected {} ({} {}) -- connecting_at {}",
|
||||
is_connected(now),
|
||||
this->connection_expiration_time,
|
||||
now,
|
||||
this->connecting_at));
|
||||
|
||||
/* also need a valid connection ID... */
|
||||
if (addr_ && !is_connected(now) && this->connecting_at == 0)
|
||||
for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
this->connecting_at = now;
|
||||
this->connection_transaction_id = tau_transaction_new();
|
||||
logtrace(log_name(), fmt::format("Trying to connect. Transaction ID is {}", this->connection_transaction_id));
|
||||
auto const ipp_enum = static_cast<tr_address_type>(ipp);
|
||||
logtrace(
|
||||
log_name(),
|
||||
fmt::format(
|
||||
"{} connected {} ({} {}) -- connecting_at {}",
|
||||
tr_ip_protocol_to_sv(ipp_enum),
|
||||
is_connected(ipp_enum, now),
|
||||
connection_expiration_time[ipp],
|
||||
now,
|
||||
connecting_at[ipp]));
|
||||
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint64(0x41727101980LL);
|
||||
buf.add_uint32(TAU_ACTION_CONNECT);
|
||||
buf.add_uint32(this->connection_transaction_id);
|
||||
// also need a valid connection ID...
|
||||
if (addr_[ipp] && !is_connected(ipp_enum, now) && connecting_at[ipp] == 0)
|
||||
{
|
||||
TR_ASSERT(addr_[ipp]->first.ss_family == tr_ip_protocol_to_af(ipp_enum));
|
||||
|
||||
this->sendto(std::data(buf), std::size(buf));
|
||||
connecting_at[ipp] = now;
|
||||
connection_transaction_id[ipp] = tau_transaction_new();
|
||||
logtrace(
|
||||
log_name(),
|
||||
fmt::format(
|
||||
"Trying to connect {}. Transaction ID is {}",
|
||||
tr_ip_protocol_to_sv(ipp_enum),
|
||||
connection_transaction_id[ipp]));
|
||||
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint64(0x41727101980LL);
|
||||
buf.add_uint32(TAU_ACTION_CONNECT);
|
||||
buf.add_uint32(connection_transaction_id[ipp]);
|
||||
|
||||
sendto(ipp_enum, std::data(buf), std::size(buf));
|
||||
}
|
||||
}
|
||||
|
||||
if (timeout_reqs)
|
||||
|
@ -401,91 +462,113 @@ struct tau_tracker
|
|||
timeout_requests(now);
|
||||
}
|
||||
|
||||
if (addr_ && is_connected(now))
|
||||
{
|
||||
send_requests();
|
||||
}
|
||||
maybe_send_requests(now);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_idle() const noexcept
|
||||
[[nodiscard]] constexpr bool is_idle() const noexcept
|
||||
{
|
||||
return std::empty(announces) && std::empty(scrapes) && !addr_pending_dns_;
|
||||
return std::empty(announces) && std::empty(scrapes);
|
||||
}
|
||||
|
||||
private:
|
||||
using Sockaddr = std::pair<sockaddr_storage, socklen_t>;
|
||||
using MaybeSockaddr = std::optional<Sockaddr>;
|
||||
|
||||
[[nodiscard]] constexpr bool is_connected(time_t now) const noexcept
|
||||
[[nodiscard]] constexpr bool is_connected(tr_address_type ip_protocol, time_t now) const noexcept
|
||||
{
|
||||
return connection_id != tau_connection_t{} && now < connection_expiration_time;
|
||||
return connection_id[ip_protocol] != tau_connection_t{} && now < connection_expiration_time[ip_protocol];
|
||||
}
|
||||
|
||||
[[nodiscard]] static MaybeSockaddr lookup(
|
||||
std::string_view const interned_log_name,
|
||||
std::string_view const interned_host,
|
||||
tr_port const port)
|
||||
[[nodiscard]] TR_CONSTEXPR20 bool is_dns_pending() const noexcept
|
||||
{
|
||||
return std::any_of(std::begin(addr_pending_dns_), std::end(addr_pending_dns_), [](auto const& o) { return !!o; });
|
||||
}
|
||||
|
||||
[[nodiscard]] TR_CONSTEXPR20 bool has_addr() const noexcept
|
||||
{
|
||||
return std::any_of(std::begin(addr_), std::end(addr_), [](auto const& o) { return !!o; });
|
||||
}
|
||||
|
||||
[[nodiscard]] MaybeSockaddr lookup(tr_address_type ip_protocol)
|
||||
{
|
||||
auto szport = std::array<char, 16>{};
|
||||
*fmt::format_to(std::data(szport), "{:d}", port.host()) = '\0';
|
||||
|
||||
auto hints = addrinfo{};
|
||||
hints.ai_family = AF_INET; // https://github.com/transmission/transmission/issues/4719
|
||||
hints.ai_family = tr_ip_protocol_to_af(ip_protocol);
|
||||
hints.ai_protocol = IPPROTO_UDP;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
|
||||
addrinfo* info = nullptr;
|
||||
auto const szhost = tr_pathbuf{ interned_host };
|
||||
auto const szhost = tr_urlbuf{ host_lookup };
|
||||
if (int const rc = getaddrinfo(szhost.c_str(), std::data(szport), &hints, &info); rc != 0)
|
||||
{
|
||||
logwarn(
|
||||
interned_log_name,
|
||||
log_name(),
|
||||
fmt::format(
|
||||
_("Couldn't look up '{address}:{port}': {error} ({error_code})"),
|
||||
fmt::arg("address", interned_host),
|
||||
_("Couldn't look up '{address}:{port}' in {ip_protocol}: {error} ({error_code})"),
|
||||
fmt::arg("address", host),
|
||||
fmt::arg("port", port.host()),
|
||||
fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol)),
|
||||
fmt::arg("error", gai_strerror(rc)),
|
||||
fmt::arg("error_code", static_cast<int>(rc))));
|
||||
return {};
|
||||
}
|
||||
auto const info_uniq = std::unique_ptr<addrinfo, void (*)(addrinfo*)>{ info,
|
||||
[](addrinfo* p) // MSVC forced my hands
|
||||
{
|
||||
freeaddrinfo(p);
|
||||
} };
|
||||
|
||||
auto ss = sockaddr_storage{};
|
||||
auto const len = info->ai_addrlen;
|
||||
memcpy(&ss, info->ai_addr, len);
|
||||
freeaddrinfo(info);
|
||||
// N.B. getaddrinfo() will return IPv4-mapped addresses by default on macOS
|
||||
auto socket_address = tr_socket_address::from_sockaddr(info->ai_addr);
|
||||
if (!socket_address || socket_address->address().is_ipv4_mapped_address())
|
||||
{
|
||||
logdbg(
|
||||
log_name(),
|
||||
fmt::format(
|
||||
"Couldn't look up '{address}:{port}' in {ip_protocol}: got invalid address",
|
||||
fmt::arg("address", host),
|
||||
fmt::arg("port", port.host()),
|
||||
fmt::arg("ip_protocol", tr_ip_protocol_to_sv(ip_protocol))));
|
||||
return {};
|
||||
}
|
||||
|
||||
logdbg(interned_log_name, "DNS lookup succeeded");
|
||||
return std::make_pair(ss, len);
|
||||
logdbg(log_name(), fmt::format("{} DNS lookup succeeded", tr_ip_protocol_to_sv(ip_protocol)));
|
||||
return socket_address->to_sockaddr();
|
||||
}
|
||||
|
||||
void failAll(bool did_connect, bool did_timeout, std::string_view errmsg)
|
||||
void fail_all(bool did_connect, bool did_timeout, std::string_view errmsg)
|
||||
{
|
||||
for (auto& req : this->scrapes)
|
||||
for (auto& req : scrapes)
|
||||
{
|
||||
req.fail(did_connect, did_timeout, errmsg);
|
||||
}
|
||||
|
||||
for (auto& req : this->announces)
|
||||
for (auto& req : announces)
|
||||
{
|
||||
req.fail(did_connect, did_timeout, errmsg);
|
||||
}
|
||||
|
||||
this->scrapes.clear();
|
||||
this->announces.clear();
|
||||
scrapes.clear();
|
||||
announces.clear();
|
||||
}
|
||||
|
||||
///
|
||||
// ---
|
||||
|
||||
void timeout_requests(time_t now)
|
||||
{
|
||||
if (this->connecting_at != 0 && this->connecting_at + ConnectionRequestTtl < now)
|
||||
for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
auto empty_buf = PayloadBuffer{};
|
||||
on_connection_response(TAU_ACTION_ERROR, empty_buf);
|
||||
if (connecting_at[ipp] != 0 && connecting_at[ipp] + ConnectionRequestTtl < now)
|
||||
{
|
||||
auto empty_buf = PayloadBuffer{};
|
||||
on_connection_response(static_cast<tr_address_type>(ipp), TAU_ACTION_ERROR, empty_buf);
|
||||
}
|
||||
}
|
||||
|
||||
timeout_requests(this->announces, now, "announce");
|
||||
timeout_requests(this->scrapes, now, "scrape");
|
||||
timeout_requests(announces, now, "announce"sv);
|
||||
timeout_requests(scrapes, now, "scrape"sv);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
|
@ -493,7 +576,7 @@ private:
|
|||
{
|
||||
for (auto it = std::begin(requests); it != std::end(requests);)
|
||||
{
|
||||
if (auto& req = *it; req.expiresAt() <= now)
|
||||
if (auto& req = *it; req.expires_at() <= now)
|
||||
{
|
||||
logtrace(log_name(), fmt::format("timeout {} req {}", name, fmt::ptr(&req)));
|
||||
req.fail(false, true, "");
|
||||
|
@ -506,37 +589,35 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
///
|
||||
// ---
|
||||
|
||||
void send_requests()
|
||||
void maybe_send_requests(time_t now)
|
||||
{
|
||||
TR_ASSERT(!addr_pending_dns_);
|
||||
TR_ASSERT(addr_);
|
||||
TR_ASSERT(this->connecting_at == 0);
|
||||
TR_ASSERT(this->connection_expiration_time > tr_time());
|
||||
TR_ASSERT(!is_dns_pending());
|
||||
if (is_dns_pending() || !has_addr())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
send_requests(this->announces);
|
||||
send_requests(this->scrapes);
|
||||
maybe_send_requests(announces, now);
|
||||
maybe_send_requests(scrapes, now);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void send_requests(std::list<T>& reqs)
|
||||
void maybe_send_requests(std::list<T>& reqs, time_t now)
|
||||
{
|
||||
auto const now = tr_time();
|
||||
|
||||
for (auto it = std::begin(reqs); it != std::end(reqs);)
|
||||
{
|
||||
auto& req = *it;
|
||||
|
||||
if (req.sent_at != 0) // it's already been sent; we're awaiting a response
|
||||
if (req.sent_at != 0 || // it's already been sent; we're awaiting a response
|
||||
!maybe_send_request(req.ip_protocol, std::data(req.payload), std::size(req.payload), now))
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
logdbg(log_name(), fmt::format("sending req {}", fmt::ptr(&req)));
|
||||
logdbg(log_name(), fmt::format("sent req {}", fmt::ptr(&req)));
|
||||
req.sent_at = now;
|
||||
send_request(std::data(req.payload), std::size(req.payload));
|
||||
|
||||
if (req.has_callback())
|
||||
{
|
||||
|
@ -549,15 +630,24 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
void send_request(std::byte const* payload, size_t payload_len)
|
||||
bool maybe_send_request(tr_address_type ip_protocol, std::byte const* payload, size_t payload_len, time_t now)
|
||||
{
|
||||
logdbg(log_name(), fmt::format("sending request w/connection id {}", this->connection_id));
|
||||
for (uint8_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
auto const ipp_enum = static_cast<tr_address_type>(ipp);
|
||||
if (addr_[ipp] && (ip_protocol == TR_AF_UNSPEC || ipp == ip_protocol) && is_connected(ipp_enum, now))
|
||||
{
|
||||
logdbg(log_name(), fmt::format("sending request w/connection id {}", connection_id[ipp]));
|
||||
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint64(this->connection_id);
|
||||
buf.add(payload, payload_len);
|
||||
auto buf = PayloadBuffer{};
|
||||
buf.add_uint64(connection_id[ipp]);
|
||||
buf.add(payload, payload_len);
|
||||
|
||||
this->sendto(std::data(buf), std::size(buf));
|
||||
sendto(ipp_enum, std::data(buf), std::size(buf));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
|
@ -566,14 +656,15 @@ public:
|
|||
return authority;
|
||||
}
|
||||
|
||||
std::string_view const authority; // interned
|
||||
std::string_view const host; // interned
|
||||
std::string_view const authority;
|
||||
std::string_view const host;
|
||||
std::string_view const host_lookup;
|
||||
tr_port const port;
|
||||
|
||||
time_t connecting_at = 0;
|
||||
time_t connection_expiration_time = 0;
|
||||
tau_connection_t connection_id = {};
|
||||
tau_transaction_t connection_transaction_id = {};
|
||||
std::array<time_t, NUM_TR_AF_INET_TYPES> connecting_at = {};
|
||||
std::array<time_t, NUM_TR_AF_INET_TYPES> connection_expiration_time = {};
|
||||
std::array<tau_connection_t, NUM_TR_AF_INET_TYPES> connection_id = {};
|
||||
std::array<tau_transaction_t, NUM_TR_AF_INET_TYPES> connection_transaction_id = {};
|
||||
|
||||
std::list<tau_announce_request> announces;
|
||||
std::list<tau_scrape_request> scrapes;
|
||||
|
@ -581,13 +672,13 @@ public:
|
|||
private:
|
||||
Mediator& mediator_;
|
||||
|
||||
std::optional<std::future<MaybeSockaddr>> addr_pending_dns_;
|
||||
std::array<std::optional<std::future<MaybeSockaddr>>, NUM_TR_AF_INET_TYPES> addr_pending_dns_;
|
||||
|
||||
MaybeSockaddr addr_;
|
||||
time_t addr_expires_at_ = 0;
|
||||
std::array<MaybeSockaddr, NUM_TR_AF_INET_TYPES> addr_ = {};
|
||||
std::array<time_t, NUM_TR_AF_INET_TYPES> addr_expires_at_ = {};
|
||||
|
||||
static constexpr auto DnsRetryIntervalSecs = time_t{ 3600 };
|
||||
static constexpr auto ConnectionRequestTtl = 30;
|
||||
static constexpr auto ConnectionRequestTtl = time_t{ 30 };
|
||||
};
|
||||
|
||||
// --- SESSION
|
||||
|
@ -602,20 +693,22 @@ public:
|
|||
|
||||
void announce(tr_announce_request const& request, tr_announce_response_func on_response) override
|
||||
{
|
||||
auto* const tracker = getTrackerFromUrl(request.announce_url);
|
||||
auto* const tracker = get_tracker_from_url(request.announce_url);
|
||||
if (tracker == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Since size of IP field is only 4 bytes long, we can only announce IPv4 addresses
|
||||
tracker->announces.emplace_back(mediator_.announce_ip(), request, std::move(on_response));
|
||||
for (ipp_t ipp = 0; ipp < NUM_TR_AF_INET_TYPES; ++ipp)
|
||||
{
|
||||
tracker->announces.emplace_back(static_cast<tr_address_type>(ipp), mediator_.announce_ip(), request, on_response);
|
||||
}
|
||||
tracker->upkeep(false);
|
||||
}
|
||||
|
||||
void scrape(tr_scrape_request const& request, tr_scrape_response_func on_response) override
|
||||
{
|
||||
auto* const tracker = getTrackerFromUrl(request.scrape_url);
|
||||
auto* const tracker = get_tracker_from_url(request.scrape_url);
|
||||
if (tracker == nullptr)
|
||||
{
|
||||
return;
|
||||
|
@ -635,7 +728,7 @@ public:
|
|||
|
||||
// @brief process an incoming udp message if it's a tracker response.
|
||||
// @return true if msg was a tracker response; false otherwise
|
||||
bool handle_message(uint8_t const* msg, size_t msglen) override
|
||||
bool handle_message(uint8_t const* msg, size_t msglen, struct sockaddr const* from, socklen_t /*fromlen*/) override
|
||||
{
|
||||
if (msglen < sizeof(uint32_t) * 2)
|
||||
{
|
||||
|
@ -647,21 +740,24 @@ public:
|
|||
buf.add(msg, msglen);
|
||||
auto const action_id = static_cast<tau_action_t>(buf.to_uint32());
|
||||
|
||||
if (!isResponseMessage(action_id, msglen))
|
||||
if (!is_response_message(action_id, msglen))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* extract the transaction_id and look for a match */
|
||||
// extract the transaction_id and look for a match
|
||||
tau_transaction_t const transaction_id = buf.to_uint32();
|
||||
|
||||
auto const socket_address = tr_socket_address::from_sockaddr(from);
|
||||
auto const ip_protocol = socket_address ? socket_address->address().type : NUM_TR_AF_INET_TYPES;
|
||||
for (auto& tracker : trackers_)
|
||||
{
|
||||
// is it a connection response?
|
||||
if (tracker.connecting_at != 0 && transaction_id == tracker.connection_transaction_id)
|
||||
if (tr_address::is_valid(ip_protocol) && tracker.connecting_at[ip_protocol] != 0 &&
|
||||
transaction_id == tracker.connection_transaction_id[ip_protocol])
|
||||
{
|
||||
logtrace(tracker.log_name(), fmt::format("{} is my connection request!", transaction_id));
|
||||
tracker.on_connection_response(action_id, buf);
|
||||
tracker.on_connection_response(ip_protocol, action_id, buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -675,9 +771,8 @@ public:
|
|||
it != std::end(reqs))
|
||||
{
|
||||
logtrace(tracker.log_name(), fmt::format("{} is an announce request!", transaction_id));
|
||||
auto req = *it;
|
||||
it = reqs.erase(it);
|
||||
req.onResponse(action_id, buf);
|
||||
it->on_response(ip_protocol, action_id, buf);
|
||||
reqs.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -692,15 +787,14 @@ public:
|
|||
it != std::end(reqs))
|
||||
{
|
||||
logtrace(tracker.log_name(), fmt::format("{} is a scrape request!", transaction_id));
|
||||
auto req = *it;
|
||||
it = reqs.erase(it);
|
||||
req.onResponse(action_id, buf);
|
||||
it->on_response(action_id, buf);
|
||||
reqs.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* no match... */
|
||||
// no match...
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -712,7 +806,7 @@ public:
|
|||
private:
|
||||
// Finds the tau_tracker struct that corresponds to this url.
|
||||
// If it doesn't exist yet, create one.
|
||||
tau_tracker* getTrackerFromUrl(tr_interned_string const announce_url)
|
||||
tau_tracker* get_tracker_from_url(tr_interned_string const announce_url)
|
||||
{
|
||||
// build a lookup key for this tracker
|
||||
auto const parsed = tr_urlParseTracker(announce_url);
|
||||
|
@ -733,12 +827,17 @@ private:
|
|||
}
|
||||
|
||||
// we don't have it -- build a new one
|
||||
auto& tracker = trackers_.emplace_back(mediator_, authority, parsed->host, tr_port::from_host(parsed->port));
|
||||
auto& tracker = trackers_.emplace_back(
|
||||
mediator_,
|
||||
authority,
|
||||
parsed->host,
|
||||
parsed->host_wo_brackets,
|
||||
tr_port::from_host(parsed->port));
|
||||
logtrace(tracker.log_name(), "New tau_tracker created");
|
||||
return &tracker;
|
||||
}
|
||||
|
||||
[[nodiscard]] static constexpr bool isResponseMessage(tau_action_t action, size_t msglen) noexcept
|
||||
[[nodiscard]] static constexpr bool is_response_message(tau_action_t action, size_t msglen) noexcept
|
||||
{
|
||||
if (action == TAU_ACTION_CONNECT)
|
||||
{
|
||||
|
|
|
@ -160,7 +160,7 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
auto const [it, is_new] = scrape_info_.try_emplace(url, url, TR_MULTISCRAPE_MAX);
|
||||
auto const [it, is_new] = scrape_info_.try_emplace(url, url, TrMultiscrapeMax);
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ public:
|
|||
|
||||
// @brief process an incoming udp message if it's a tracker response.
|
||||
// @return true if msg was a tracker response; false otherwise
|
||||
virtual bool handle_message(uint8_t const* msg, size_t msglen) = 0;
|
||||
virtual bool handle_message(uint8_t const* msg, size_t msglen, struct sockaddr const* from, socklen_t fromlen) = 0;
|
||||
|
||||
[[nodiscard]] virtual bool is_idle() const noexcept = 0;
|
||||
};
|
||||
|
|
|
@ -433,16 +433,6 @@ namespace
|
|||
namespace is_valid_for_peers_helpers
|
||||
{
|
||||
|
||||
[[nodiscard]] constexpr auto is_ipv4_mapped_address(tr_address const& addr)
|
||||
{
|
||||
return addr.is_ipv6() && IN6_IS_ADDR_V4MAPPED(&addr.addr.addr6);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr auto is_ipv6_link_local_address(tr_address const& addr)
|
||||
{
|
||||
return addr.is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr.addr6);
|
||||
}
|
||||
|
||||
/* isMartianAddr was written by Juliusz Chroboczek,
|
||||
and is covered under the same license as third-party/dht/dht.c. */
|
||||
[[nodiscard]] auto is_martian_addr(tr_address const& addr, tr_peer_from from)
|
||||
|
@ -811,7 +801,7 @@ bool tr_socket_address::is_valid_for_peers(tr_peer_from from) const noexcept
|
|||
{
|
||||
using namespace is_valid_for_peers_helpers;
|
||||
|
||||
return is_valid() && !std::empty(port_) && !is_ipv6_link_local_address(address_) && !is_ipv4_mapped_address(address_) &&
|
||||
return is_valid() && !std::empty(port_) && !address_.is_ipv6_link_local_address() && !address_.is_ipv4_mapped_address() &&
|
||||
!is_martian_addr(address_, from);
|
||||
}
|
||||
|
||||
|
|
|
@ -146,9 +146,10 @@ private:
|
|||
|
||||
enum tr_address_type : uint8_t
|
||||
{
|
||||
TR_AF_INET,
|
||||
TR_AF_INET = 0,
|
||||
TR_AF_INET6,
|
||||
NUM_TR_AF_INET_TYPES
|
||||
NUM_TR_AF_INET_TYPES,
|
||||
TR_AF_UNSPEC = NUM_TR_AF_INET_TYPES
|
||||
};
|
||||
|
||||
std::string_view tr_ip_protocol_to_sv(tr_address_type type);
|
||||
|
@ -240,6 +241,16 @@ struct tr_address
|
|||
|
||||
[[nodiscard]] bool is_global_unicast_address() const noexcept;
|
||||
|
||||
[[nodiscard]] constexpr bool is_ipv4_mapped_address() const noexcept
|
||||
{
|
||||
return is_ipv6() && IN6_IS_ADDR_V4MAPPED(&addr.addr6);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool is_ipv6_link_local_address() const noexcept
|
||||
{
|
||||
return is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr6);
|
||||
}
|
||||
|
||||
tr_address_type type = NUM_TR_AF_INET_TYPES;
|
||||
union
|
||||
{
|
||||
|
|
|
@ -126,7 +126,7 @@ void event_callback(evutil_socket_t s, [[maybe_unused]] short type, void* vsessi
|
|||
}
|
||||
else if (n_read >= 8 && buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] <= 3)
|
||||
{
|
||||
if (!session->announcer_udp_->handle_message(std::data(buf), n_read))
|
||||
if (!session->announcer_udp_->handle_message(std::data(buf), n_read, from_sa, fromlen))
|
||||
{
|
||||
tr_logAddTrace("Couldn't parse UDP tracker packet.");
|
||||
}
|
||||
|
|
|
@ -291,6 +291,19 @@ std::string_view getSiteName(std::string_view host)
|
|||
|
||||
return host;
|
||||
}
|
||||
|
||||
// Not part of the RFC3986 standard, but included for convenience
|
||||
// when using the result with API that does not accept IPv6 address
|
||||
// strings that are wrapped in square brackets (e.g. inet_pton())
|
||||
std::string_view getHostWoBrackets(std::string_view host)
|
||||
{
|
||||
if (tr_strv_starts_with(host, '['))
|
||||
{
|
||||
host.remove_prefix(1);
|
||||
host.remove_suffix(1);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::optional<tr_url_parsed_t> tr_urlParse(std::string_view url)
|
||||
|
@ -363,6 +376,7 @@ std::optional<tr_url_parsed_t> tr_urlParse(std::string_view url)
|
|||
{
|
||||
parsed.host = tr_strv_sep(&remain, ':');
|
||||
}
|
||||
parsed.host_wo_brackets = getHostWoBrackets(parsed.host);
|
||||
parsed.sitename = getSiteName(parsed.host);
|
||||
parsed.port = parsePort(!std::empty(remain) ? remain : getPortForScheme(parsed.scheme));
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ struct tr_url_parsed_t
|
|||
std::string_view scheme; // "http"
|
||||
std::string_view authority; // "example.com:80"
|
||||
std::string_view host; // "example.com"
|
||||
std::string_view host_wo_brackets; // "example.com" ("[::1]" -> "::1")
|
||||
std::string_view sitename; // "example"
|
||||
std::string_view path; // /"over/there"
|
||||
std::string_view query; // "name=ferret"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
@ -45,6 +46,9 @@
|
|||
|
||||
using namespace std::literals;
|
||||
|
||||
using tau_connection_t = uint64_t;
|
||||
using tau_transaction_t = uint32_t;
|
||||
|
||||
using MessageBuffer = libtransmission::StackBuffer<4096, std::byte>;
|
||||
|
||||
class AnnouncerUdpTest : public ::testing::Test
|
||||
|
@ -132,7 +136,7 @@ protected:
|
|||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] static uint32_t parseConnectionRequest(std::vector<char> const& data)
|
||||
[[nodiscard]] static tau_transaction_t parseConnectionRequest(std::vector<char> const& data)
|
||||
{
|
||||
auto buf = MessageBuffer(data);
|
||||
EXPECT_EQ(ProtocolId, buf.to_uint64());
|
||||
|
@ -169,7 +173,7 @@ protected:
|
|||
return std::make_pair(buildScrapeRequestFromResponse(response), response);
|
||||
}
|
||||
|
||||
[[nodiscard]] static auto parseScrapeRequest(std::vector<char> const& data, uint64_t expected_connection_id)
|
||||
[[nodiscard]] static auto parseScrapeRequest(std::vector<char> const& data, tau_connection_t expected_connection_id)
|
||||
{
|
||||
auto buf = MessageBuffer(data);
|
||||
EXPECT_EQ(expected_connection_id, buf.to_uint64());
|
||||
|
@ -187,13 +191,19 @@ protected:
|
|||
|
||||
[[nodiscard]] static auto waitForAnnouncerToSendMessage(MockMediator& mediator)
|
||||
{
|
||||
libtransmission::test::waitFor(mediator.eventBase(), [&mediator]() { return !std::empty(mediator.sent_); });
|
||||
EXPECT_TRUE(
|
||||
libtransmission::test::waitFor(mediator.eventBase(), [&mediator]() { return !std::empty(mediator.sent_); }));
|
||||
auto buf = std::move(mediator.sent_.back().buf_);
|
||||
mediator.sent_.pop_back();
|
||||
return buf;
|
||||
}
|
||||
|
||||
[[nodiscard]] static bool sendError(tr_announcer_udp& announcer, uint32_t transaction_id, std::string_view errmsg)
|
||||
[[nodiscard]] static bool sendError(
|
||||
tr_announcer_udp& announcer,
|
||||
tau_transaction_t transaction_id,
|
||||
std::string_view errmsg,
|
||||
struct sockaddr const* from,
|
||||
socklen_t fromlen)
|
||||
{
|
||||
auto buf = MessageBuffer{};
|
||||
buf.add_uint32(ErrorAction);
|
||||
|
@ -204,21 +214,25 @@ protected:
|
|||
auto arr = std::array<uint8_t, 256>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
|
||||
return announcer.handle_message(std::data(arr), response_size);
|
||||
return announcer.handle_message(std::data(arr), response_size, from, fromlen);
|
||||
}
|
||||
|
||||
[[nodiscard]] static auto sendConnectionResponse(tr_announcer_udp& announcer, uint32_t transaction_id)
|
||||
[[nodiscard]] static auto sendConnectionResponse(
|
||||
tr_announcer_udp& announcer,
|
||||
tau_transaction_t transaction_id,
|
||||
struct sockaddr const* from,
|
||||
socklen_t fromlen)
|
||||
{
|
||||
auto const connection_id = tr_rand_obj<uint64_t>();
|
||||
auto const connection_id = tr_rand_obj<tau_connection_t>();
|
||||
auto buf = MessageBuffer{};
|
||||
buf.add_uint32(ConnectAction);
|
||||
buf.add_uint32(transaction_id);
|
||||
buf.add_uint64(connection_id);
|
||||
|
||||
auto arr = std::array<uint8_t, 128>{};
|
||||
auto arr = std::array<uint8_t, 16>{};
|
||||
auto response_size = std::size(buf);
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_TRUE(announcer.handle_message(std::data(arr), response_size));
|
||||
EXPECT_TRUE(announcer.handle_message(std::data(arr), response_size, from, fromlen));
|
||||
|
||||
return connection_id;
|
||||
}
|
||||
|
@ -227,7 +241,7 @@ protected:
|
|||
{
|
||||
uint64_t connection_id = 0;
|
||||
uint32_t action = 0; // 1: announce
|
||||
uint32_t transaction_id = 0;
|
||||
tau_transaction_t transaction_id = 0;
|
||||
tr_sha1_digest_t info_hash = {};
|
||||
tr_peer_id_t peer_id = {};
|
||||
uint64_t downloaded = 0;
|
||||
|
@ -307,6 +321,16 @@ protected:
|
|||
return timer;
|
||||
}
|
||||
|
||||
static auto sockaddrFromUrl(std::string_view tracker_url)
|
||||
{
|
||||
auto parsed_url = tr_urlParse(tracker_url);
|
||||
EXPECT_TRUE(parsed_url);
|
||||
auto addr = tr_address::from_string(parsed_url->host);
|
||||
EXPECT_TRUE(addr);
|
||||
|
||||
return tr_socket_address{ *addr, tr_port::from_host(parsed_url->port) }.to_sockaddr();
|
||||
}
|
||||
|
||||
std::unique_ptr<tr_net_init_mgr> init_mgr_;
|
||||
|
||||
// https://www.bittorrent.org/beps/bep_0015.html
|
||||
|
@ -337,12 +361,16 @@ TEST_F(AnnouncerUdpTest, canScrape)
|
|||
auto response = std::optional<tr_scrape_response>{};
|
||||
announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; });
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(request.scrape_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
// The announcer should have sent a UDP connection request.
|
||||
// Inspect that request for validity.
|
||||
auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
|
||||
// Have the tracker respond to the request
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id);
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen);
|
||||
|
||||
// The announcer should have sent a UDP scrape request.
|
||||
// Inspect that request for validity.
|
||||
|
@ -359,7 +387,7 @@ TEST_F(AnnouncerUdpTest, canScrape)
|
|||
auto response_size = std::size(buf);
|
||||
auto arr = std::array<uint8_t, 256>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size));
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
|
@ -413,13 +441,17 @@ TEST_F(AnnouncerUdpTest, canMultiScrape)
|
|||
expected_response.scrape_url = DefaultScrapeUrl;
|
||||
expected_response.min_request_interval = 0;
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
auto request = buildScrapeRequestFromResponse(expected_response);
|
||||
auto response = std::optional<tr_scrape_response>{};
|
||||
announcer->scrape(request, [&response](tr_scrape_response const& resp) { response = resp; });
|
||||
|
||||
// Announcer will request a connection. Verify and grant the request
|
||||
auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id);
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen);
|
||||
|
||||
// The announcer should have sent a UDP scrape request.
|
||||
// Inspect that request for validity.
|
||||
|
@ -439,7 +471,7 @@ TEST_F(AnnouncerUdpTest, canMultiScrape)
|
|||
auto response_size = std::size(buf);
|
||||
auto arr = std::array<uint8_t, 256>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size));
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// Confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
|
@ -463,6 +495,10 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError)
|
|||
expected_response.min_request_interval = 0;
|
||||
expected_response.errmsg = "Unrecognized info-hash";
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
// build the request
|
||||
auto request = buildScrapeRequestFromResponse(expected_response);
|
||||
|
||||
|
@ -480,7 +516,7 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError)
|
|||
auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
|
||||
// Have the tracker respond to the request
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id);
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen);
|
||||
|
||||
// The announcer should have sent a UDP scrape request.
|
||||
// Inspect that request for validity.
|
||||
|
@ -489,7 +525,7 @@ TEST_F(AnnouncerUdpTest, canHandleScrapeError)
|
|||
connection_id);
|
||||
|
||||
// Have the tracker respond to the request with an "unable to scrape" error
|
||||
EXPECT_TRUE(sendError(*announcer, scrape_transaction_id, expected_response.errmsg));
|
||||
EXPECT_TRUE(sendError(*announcer, scrape_transaction_id, expected_response.errmsg, from_ptr, fromlen));
|
||||
|
||||
// confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
|
@ -513,6 +549,10 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError)
|
|||
expected_response.min_request_interval = 0;
|
||||
expected_response.errmsg = "Unable to Connect";
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(expected_response.scrape_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
// build the announcer
|
||||
auto mediator = MockMediator{};
|
||||
auto announcer = tr_announcer_udp::create(mediator);
|
||||
|
@ -529,7 +569,7 @@ TEST_F(AnnouncerUdpTest, canHandleConnectError)
|
|||
auto transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
|
||||
// Have the tracker respond to the request with an "unable to connect" error
|
||||
EXPECT_TRUE(sendError(*announcer, transaction_id, expected_response.errmsg));
|
||||
EXPECT_TRUE(sendError(*announcer, transaction_id, expected_response.errmsg, from_ptr, fromlen));
|
||||
|
||||
// Confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
|
@ -545,6 +585,10 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage)
|
|||
request.info_hash_count = 1;
|
||||
request.info_hash[0] = tr_rand_obj<tr_sha1_digest_t>();
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(request.scrape_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
// build the announcer
|
||||
auto mediator = MockMediator{};
|
||||
auto announcer = tr_announcer_udp::create(mediator);
|
||||
|
@ -566,7 +610,7 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage)
|
|||
auto response_size = std::size(buf);
|
||||
auto arr = std::array<uint8_t, 256>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size));
|
||||
EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// send a connection response but with an *invalid* action
|
||||
buf.clear();
|
||||
|
@ -575,15 +619,15 @@ TEST_F(AnnouncerUdpTest, handleMessageReturnsFalseOnInvalidMessage)
|
|||
buf.add_uint64(tr_rand_obj<uint64_t>());
|
||||
response_size = std::size(buf);
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size));
|
||||
EXPECT_FALSE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// but after discarding invalid messages,
|
||||
// a valid connection response should still work
|
||||
auto const connection_id = sendConnectionResponse(*announcer, transaction_id);
|
||||
auto const connection_id = sendConnectionResponse(*announcer, transaction_id, from_ptr, fromlen);
|
||||
EXPECT_NE(0, connection_id);
|
||||
}
|
||||
|
||||
TEST_F(AnnouncerUdpTest, canAnnounce)
|
||||
TEST_F(AnnouncerUdpTest, canAnnounceIPv4)
|
||||
{
|
||||
static auto constexpr Interval = uint32_t{ 3600 };
|
||||
static auto constexpr Leechers = uint32_t{ 10 };
|
||||
|
@ -608,6 +652,10 @@ TEST_F(AnnouncerUdpTest, canAnnounce)
|
|||
request.peer_id = tr_peerIdInit();
|
||||
request.info_hash = tr_rand_obj<tr_sha1_digest_t>();
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(request.announce_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
auto expected_response = tr_announce_response{};
|
||||
expected_response.info_hash = request.info_hash;
|
||||
expected_response.did_connect = true;
|
||||
|
@ -634,7 +682,7 @@ TEST_F(AnnouncerUdpTest, canAnnounce)
|
|||
|
||||
// Announcer will request a connection. Verify and grant the request
|
||||
auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id);
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen);
|
||||
|
||||
// The announcer should have sent a UDP announce request.
|
||||
// Inspect that request for validity.
|
||||
|
@ -658,7 +706,96 @@ TEST_F(AnnouncerUdpTest, canAnnounce)
|
|||
auto response_size = std::size(buf);
|
||||
auto arr = std::array<uint8_t, 512>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size));
|
||||
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// Confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
assert(response.has_value());
|
||||
expectEqual(expected_response, *response);
|
||||
}
|
||||
|
||||
TEST_F(AnnouncerUdpTest, canAnnounceIPv6)
|
||||
{
|
||||
static auto constexpr Interval = uint32_t{ 3600 };
|
||||
static auto constexpr Leechers = uint32_t{ 10 };
|
||||
static auto constexpr Seeders = uint32_t{ 20 };
|
||||
auto const addresses = std::array<tr_socket_address, 3>{ {
|
||||
{ tr_address::from_string("fd12:3456:789a:1::1").value_or(tr_address{}), tr_port::from_host(128) },
|
||||
{ tr_address::from_string("fd12:3456:789a:1::2").value_or(tr_address{}), tr_port::from_host(2021) },
|
||||
{ tr_address::from_string("fd12:3456:789a:1::3").value_or(tr_address{}), tr_port::from_host(2022) },
|
||||
} };
|
||||
|
||||
auto request = tr_announce_request{};
|
||||
request.event = TR_ANNOUNCE_EVENT_STARTED;
|
||||
request.port = tr_port::from_host(80);
|
||||
request.key = 0xCAFE;
|
||||
request.numwant = 20;
|
||||
request.up = 1;
|
||||
request.down = 2;
|
||||
request.corrupt = 3;
|
||||
request.leftUntilComplete = 100;
|
||||
request.announce_url = "https://[::1]/announce";
|
||||
request.tracker_id = "fnord";
|
||||
request.peer_id = tr_peerIdInit();
|
||||
request.info_hash = tr_rand_obj<tr_sha1_digest_t>();
|
||||
|
||||
// Obtain the source socket address from tracker url
|
||||
auto [from, fromlen] = sockaddrFromUrl(request.announce_url);
|
||||
auto const* const from_ptr = reinterpret_cast<struct sockaddr*>(&from);
|
||||
|
||||
auto expected_response = tr_announce_response{};
|
||||
expected_response.info_hash = request.info_hash;
|
||||
expected_response.did_connect = true;
|
||||
expected_response.did_timeout = false;
|
||||
expected_response.interval = Interval;
|
||||
expected_response.min_interval = 0; // not specified in UDP announce
|
||||
expected_response.seeders = Seeders;
|
||||
expected_response.leechers = Leechers;
|
||||
expected_response.downloads = std::nullopt; // not specified in UDP announce
|
||||
expected_response.pex = {};
|
||||
expected_response.pex6 = std::vector<tr_pex>{ tr_pex{ addresses[0] }, tr_pex{ addresses[1] }, tr_pex{ addresses[2] } };
|
||||
expected_response.errmsg = {};
|
||||
expected_response.warning = {};
|
||||
expected_response.tracker_id = {}; // not specified in UDP announce
|
||||
expected_response.external_ip = {};
|
||||
|
||||
// build the announcer
|
||||
auto mediator = MockMediator{};
|
||||
auto announcer = tr_announcer_udp::create(mediator);
|
||||
auto upkeep_timer = createUpkeepTimer(mediator, announcer);
|
||||
|
||||
auto response = std::optional<tr_announce_response>{};
|
||||
announcer->announce(request, [&response](tr_announce_response const& resp) { response = resp; });
|
||||
|
||||
// Announcer will request a connection. Verify and grant the request
|
||||
auto connect_transaction_id = parseConnectionRequest(waitForAnnouncerToSendMessage(mediator));
|
||||
auto const connection_id = sendConnectionResponse(*announcer, connect_transaction_id, from_ptr, fromlen);
|
||||
|
||||
// The announcer should have sent a UDP announce request.
|
||||
// Inspect that request for validity.
|
||||
auto udp_ann_req = parseAnnounceRequest(waitForAnnouncerToSendMessage(mediator), connection_id);
|
||||
expectEqual(request, udp_ann_req);
|
||||
|
||||
// Have the tracker respond to the request
|
||||
auto buf = MessageBuffer{};
|
||||
buf.add_uint32(AnnounceAction);
|
||||
buf.add_uint32(udp_ann_req.transaction_id);
|
||||
buf.add_uint32(expected_response.interval);
|
||||
buf.add_uint32(expected_response.leechers.value_or(-1));
|
||||
buf.add_uint32(expected_response.seeders.value_or(-1));
|
||||
for (auto const& [addr, port] : addresses)
|
||||
{
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access)
|
||||
buf.add(&addr.addr.addr6.s6_addr, sizeof(addr.addr.addr6.s6_addr));
|
||||
buf.add_uint16(port.host());
|
||||
}
|
||||
|
||||
auto response_size = std::size(buf);
|
||||
auto arr = std::array<uint8_t, 512>{};
|
||||
buf.to_buf(std::data(arr), response_size);
|
||||
|
||||
EXPECT_TRUE(announcer->handle_message(std::data(arr), response_size, from_ptr, fromlen));
|
||||
|
||||
// Confirm that announcer processed the response
|
||||
EXPECT_TRUE(response.has_value());
|
||||
|
|
Loading…
Reference in a new issue