mirror of
https://github.com/transmission/transmission
synced 2025-03-04 02:28:03 +00:00
Implement latest version of BEP-7 for HTTP requests (#1661)
* Implement BEP-7 for HTTP announce (fixes #1659)
This commit is contained in:
parent
eb36788253
commit
42198afc5f
3 changed files with 186 additions and 32 deletions
|
@ -9,9 +9,12 @@
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
#include <event2/http.h> /* for HTTP_OK */
|
#include <event2/http.h> /* for HTTP_OK */
|
||||||
|
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
|
@ -29,6 +32,7 @@
|
||||||
#include "peer-mgr.h" /* pex */
|
#include "peer-mgr.h" /* pex */
|
||||||
#include "quark.h"
|
#include "quark.h"
|
||||||
#include "torrent.h"
|
#include "torrent.h"
|
||||||
|
#include "tr-assert.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "web-utils.h"
|
#include "web-utils.h"
|
||||||
#include "web.h"
|
#include "web.h"
|
||||||
|
@ -98,26 +102,28 @@ static tr_urlbuf announce_url_new(tr_session const* session, tr_announce_request
|
||||||
fmt::format_to(out, "&trackerid={}", req->tracker_id);
|
fmt::format_to(out, "&trackerid={}", req->tracker_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* There are two incompatible techniques for announcing an IPv6 address.
|
|
||||||
BEP-7 suggests adding an "ipv6=" parameter to the announce URL,
|
|
||||||
while OpenTracker requires that peers announce twice, once over IPv4
|
|
||||||
and once over IPv6.
|
|
||||||
|
|
||||||
To be safe, we should do both: add the "ipv6=" parameter and
|
|
||||||
announce twice. At any rate, we're already computing our IPv6
|
|
||||||
address (for the LTEP handshake), so this comes for free. */
|
|
||||||
|
|
||||||
if (auto const* const ipv6 = tr_globalIPv6(session); ipv6 != nullptr)
|
|
||||||
{
|
|
||||||
auto ipv6_readable = std::array<char, INET6_ADDRSTRLEN>{};
|
|
||||||
evutil_inet_ntop(AF_INET6, ipv6, std::data(ipv6_readable), std::size(ipv6_readable));
|
|
||||||
fmt::format_to(out, "&ipv6=");
|
|
||||||
tr_http_escape(out, std::data(ipv6_readable), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string format_ipv4_url_arg(tr_address const& ipv4_address)
|
||||||
|
{
|
||||||
|
std::array<char, INET_ADDRSTRLEN> readable;
|
||||||
|
evutil_inet_ntop(AF_INET, &ipv4_address.addr, readable.data(), readable.size());
|
||||||
|
|
||||||
|
return "&ipv4="s + readable.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string format_ipv6_url_arg(unsigned char const* ipv6_address)
|
||||||
|
{
|
||||||
|
std::array<char, INET6_ADDRSTRLEN> readable;
|
||||||
|
evutil_inet_ntop(AF_INET6, ipv6_address, readable.data(), readable.size());
|
||||||
|
|
||||||
|
auto arg = "&ipv6="s;
|
||||||
|
tr_http_escape(std::back_inserter(arg), readable.data(), true);
|
||||||
|
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
static void verboseLog(std::string_view description, tr_direction direction, std::string_view message)
|
static void verboseLog(std::string_view description, tr_direction direction, std::string_view message)
|
||||||
{
|
{
|
||||||
auto& out = std::cerr;
|
auto& out = std::cerr;
|
||||||
|
@ -283,18 +289,24 @@ void tr_announcerParseHttpAnnounceResponse(tr_announce_response& response, std::
|
||||||
|
|
||||||
struct announce_data
|
struct announce_data
|
||||||
{
|
{
|
||||||
tr_announce_response response;
|
tr_sha1_digest_t info_hash;
|
||||||
|
std::optional<tr_announce_response> previous_response;
|
||||||
|
|
||||||
tr_announce_response_func response_func;
|
tr_announce_response_func response_func;
|
||||||
void* response_func_user_data;
|
void* response_func_user_data;
|
||||||
|
bool http_success = false;
|
||||||
|
|
||||||
|
uint8_t requests_sent_count;
|
||||||
|
uint8_t requests_answered_count;
|
||||||
|
|
||||||
char log_name[128];
|
char log_name[128];
|
||||||
};
|
};
|
||||||
|
|
||||||
static void onAnnounceDone(tr_web::FetchResponse const& web_response)
|
static bool handleAnnounceResponse(tr_web::FetchResponse const& web_response, tr_announce_response* const response)
|
||||||
{
|
{
|
||||||
auto const& [status, body, did_connect, did_timeout, vdata] = web_response;
|
auto const& [status, body, did_connect, did_timeout, vdata] = web_response;
|
||||||
auto* data = static_cast<struct announce_data*>(vdata);
|
auto* data = static_cast<struct announce_data*>(vdata);
|
||||||
|
|
||||||
tr_announce_response* const response = &data->response;
|
|
||||||
response->did_connect = did_connect;
|
response->did_connect = did_connect;
|
||||||
response->did_timeout = did_timeout;
|
response->did_timeout = did_timeout;
|
||||||
tr_logAddTrace("Got announce response", data->log_name);
|
tr_logAddTrace("Got announce response", data->log_name);
|
||||||
|
@ -303,11 +315,11 @@ static void onAnnounceDone(tr_web::FetchResponse const& web_response)
|
||||||
{
|
{
|
||||||
auto const* const response_str = tr_webGetResponseStr(status);
|
auto const* const response_str = tr_webGetResponseStr(status);
|
||||||
response->errmsg = fmt::format(FMT_STRING("Tracker HTTP response {:d} ({:s}"), status, response_str);
|
response->errmsg = fmt::format(FMT_STRING("Tracker HTTP response {:d} ({:s}"), status, response_str);
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
tr_announcerParseHttpAnnounceResponse(*response, body, data->log_name);
|
||||||
tr_announcerParseHttpAnnounceResponse(*response, body, data->log_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!std::empty(response->pex6))
|
if (!std::empty(response->pex6))
|
||||||
{
|
{
|
||||||
|
@ -319,12 +331,61 @@ static void onAnnounceDone(tr_web::FetchResponse const& web_response)
|
||||||
tr_logAddTrace(fmt::format("got a peers length of {}", std::size(response->pex)), data->log_name);
|
tr_logAddTrace(fmt::format("got a peers length of {}", std::size(response->pex)), data->log_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data->response_func != nullptr)
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onAnnounceDone(tr_web::FetchResponse const& web_response)
|
||||||
|
{
|
||||||
|
auto const& [status, body, did_connect, did_timeout, vdata] = web_response;
|
||||||
|
auto* data = static_cast<struct announce_data*>(vdata);
|
||||||
|
|
||||||
|
++data->requests_answered_count;
|
||||||
|
|
||||||
|
// If another request already succeeded (or we don't have a registered callback),
|
||||||
|
// skip processing this response:
|
||||||
|
if (!data->http_success && data->response_func != nullptr)
|
||||||
{
|
{
|
||||||
data->response_func(&data->response, data->response_func_user_data);
|
tr_announce_response response;
|
||||||
|
response.info_hash = data->info_hash;
|
||||||
|
|
||||||
|
data->http_success = handleAnnounceResponse(web_response, &response);
|
||||||
|
|
||||||
|
if (data->http_success)
|
||||||
|
{
|
||||||
|
data->response_func(&response, data->response_func_user_data);
|
||||||
|
}
|
||||||
|
else if (data->requests_answered_count == data->requests_sent_count)
|
||||||
|
{
|
||||||
|
auto const* response_used = &response;
|
||||||
|
|
||||||
|
// All requests have been answered, but none were successfull.
|
||||||
|
// Choose the one that went further to report.
|
||||||
|
if (data->previous_response && !response.did_connect && !response.did_timeout)
|
||||||
|
{
|
||||||
|
response_used = &*data->previous_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
data->response_func(response_used, data->response_func_user_data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// There is still one request pending that might succeed, so store
|
||||||
|
// the response for later. There is only room for 1 previous response,
|
||||||
|
// because there can be at most 2 requests.
|
||||||
|
TR_ASSERT(!data->previous_response);
|
||||||
|
data->previous_response = std::move(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tr_logAddTrace("Ignoring redundant announce response", data->log_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete data;
|
// Free data if no more responses are expected:
|
||||||
|
if (data->requests_answered_count == data->requests_sent_count)
|
||||||
|
{
|
||||||
|
delete data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void tr_tracker_http_announce(
|
void tr_tracker_http_announce(
|
||||||
|
@ -336,17 +397,86 @@ void tr_tracker_http_announce(
|
||||||
auto* const d = new announce_data();
|
auto* const d = new announce_data();
|
||||||
d->response_func = response_func;
|
d->response_func = response_func;
|
||||||
d->response_func_user_data = response_func_user_data;
|
d->response_func_user_data = response_func_user_data;
|
||||||
d->response.info_hash = request->info_hash;
|
d->info_hash = request->info_hash;
|
||||||
tr_strlcpy(d->log_name, request->log_name, sizeof(d->log_name));
|
tr_strlcpy(d->log_name, request->log_name, sizeof(d->log_name));
|
||||||
|
|
||||||
auto const url = announce_url_new(session, request);
|
/* There are two alternative techniques for announcing both IPv4 and
|
||||||
tr_logAddTrace(fmt::format("Sending announce to libcurl: '{}'", url), request->log_name);
|
IPv6 addresses. Previous version of BEP-7 suggests adding "ipv4="
|
||||||
|
and "ipv6=" parameters to the announce URL, while OpenTracker and
|
||||||
|
newer version of BEP-7 requires that peers announce once per each
|
||||||
|
public address they want to use.
|
||||||
|
|
||||||
auto options = tr_web::FetchOptions{ url.sv(), onAnnounceDone, d };
|
We should ensure that we send the announce both via IPv6 and IPv4,
|
||||||
|
and to be safe we also add the "ipv6=" and "ipv4=" parameters, if
|
||||||
|
we already have them. Our global IPv6 address is computed for the
|
||||||
|
LTEP handshake, so this comes for free. Our public IPv4 address
|
||||||
|
may have been returned from a previous announce and stored in the
|
||||||
|
session.
|
||||||
|
*/
|
||||||
|
auto url_base = announce_url_new(session, request);
|
||||||
|
|
||||||
|
auto options = tr_web::FetchOptions{ url_base.sv(), onAnnounceDone, d };
|
||||||
options.timeout_secs = 90L;
|
options.timeout_secs = 90L;
|
||||||
options.sndbuf = 4096;
|
options.sndbuf = 4096;
|
||||||
options.rcvbuf = 4096;
|
options.rcvbuf = 4096;
|
||||||
session->web->fetch(std::move(options));
|
|
||||||
|
auto do_make_request = [&](std::string_view const& protocol_name, tr_web::FetchOptions&& opt)
|
||||||
|
{
|
||||||
|
tr_logAddTrace(fmt::format("Sending {} announce to libcurl: '{}'", protocol_name, opt.url), request->log_name);
|
||||||
|
session->web->fetch(std::move(opt));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto ipv6 = tr_globalIPv6(session);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Before Curl 7.77.0, if we explicitly choose the IP version we want
|
||||||
|
* to use, it is still possible that the wrong one is used. The workaround
|
||||||
|
* is expensive (disabling DNS cache), so instead we have to make do with
|
||||||
|
* a request that we don't know if will go through IPv6 or IPv4.
|
||||||
|
*/
|
||||||
|
static bool const use_curl_workaround = curl_version_info(CURLVERSION_NOW)->version_num < CURL_VERSION_BITS(7, 77, 0);
|
||||||
|
if (use_curl_workaround)
|
||||||
|
{
|
||||||
|
if (ipv6 != nullptr)
|
||||||
|
{
|
||||||
|
if (auto public_ipv4 = session->externalIP(); public_ipv4.has_value())
|
||||||
|
{
|
||||||
|
options.url += format_ipv4_url_arg(*public_ipv4);
|
||||||
|
}
|
||||||
|
options.url += format_ipv6_url_arg(ipv6);
|
||||||
|
}
|
||||||
|
|
||||||
|
d->requests_sent_count = 1;
|
||||||
|
do_make_request(""sv, std::move(options));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ipv6 != nullptr)
|
||||||
|
{
|
||||||
|
d->requests_sent_count = 2;
|
||||||
|
|
||||||
|
// First try to send the announce via IPv4:
|
||||||
|
auto ipv4_options = options;
|
||||||
|
// Set the "&ipv6=" argument
|
||||||
|
ipv4_options.url += format_ipv6_url_arg(ipv6);
|
||||||
|
// Set protocol to IPv4
|
||||||
|
ipv4_options.ip_proto = tr_web::FetchOptions::IPProtocol::V4;
|
||||||
|
do_make_request("IPv4"sv, std::move(ipv4_options));
|
||||||
|
|
||||||
|
// Then maybe set the "&ipv4=..." part and try to send via IPv6:
|
||||||
|
if (auto public_ipv4 = session->externalIP(); public_ipv4.has_value())
|
||||||
|
{
|
||||||
|
options.url += format_ipv4_url_arg(*public_ipv4);
|
||||||
|
}
|
||||||
|
options.ip_proto = tr_web::FetchOptions::IPProtocol::V6;
|
||||||
|
do_make_request("IPv6"sv, std::move(options));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
d->requests_sent_count = 1;
|
||||||
|
do_make_request(""sv, std::move(options));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/****
|
/****
|
||||||
|
|
|
@ -232,6 +232,19 @@ private:
|
||||||
return options.timeout_secs;
|
return options.timeout_secs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto ipProtocol() const
|
||||||
|
{
|
||||||
|
switch (options.ip_proto)
|
||||||
|
{
|
||||||
|
case FetchOptions::IPProtocol::V4:
|
||||||
|
return CURL_IPRESOLVE_V4;
|
||||||
|
case FetchOptions::IPProtocol::V6:
|
||||||
|
return CURL_IPRESOLVE_V6;
|
||||||
|
default:
|
||||||
|
return CURL_IPRESOLVE_WHATEVER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void done()
|
void done()
|
||||||
{
|
{
|
||||||
if (options.done_func == nullptr)
|
if (options.done_func == nullptr)
|
||||||
|
@ -334,6 +347,7 @@ private:
|
||||||
(void)curl_easy_setopt(e, CURLOPT_MAXREDIRS, -1L);
|
(void)curl_easy_setopt(e, CURLOPT_MAXREDIRS, -1L);
|
||||||
(void)curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L);
|
(void)curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L);
|
||||||
(void)curl_easy_setopt(e, CURLOPT_PRIVATE, task);
|
(void)curl_easy_setopt(e, CURLOPT_PRIVATE, task);
|
||||||
|
(void)curl_easy_setopt(e, CURLOPT_IPRESOLVE, task->ipProtocol());
|
||||||
|
|
||||||
#ifdef USE_LIBCURL_SOCKOPT
|
#ifdef USE_LIBCURL_SOCKOPT
|
||||||
(void)curl_easy_setopt(e, CURLOPT_SOCKOPTFUNCTION, onSocketCreated);
|
(void)curl_easy_setopt(e, CURLOPT_SOCKOPTFUNCTION, onSocketCreated);
|
||||||
|
|
|
@ -34,6 +34,13 @@ public:
|
||||||
class FetchOptions
|
class FetchOptions
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum class IPProtocol
|
||||||
|
{
|
||||||
|
ANY,
|
||||||
|
V4,
|
||||||
|
V6,
|
||||||
|
};
|
||||||
|
|
||||||
FetchOptions(std::string_view url_in, FetchDoneFunc&& done_func_in, void* done_func_user_data_in)
|
FetchOptions(std::string_view url_in, FetchDoneFunc&& done_func_in, void* done_func_user_data_in)
|
||||||
: url{ url_in }
|
: url{ url_in }
|
||||||
, done_func{ std::move(done_func_in) }
|
, done_func{ std::move(done_func_in) }
|
||||||
|
@ -72,6 +79,9 @@ public:
|
||||||
// the buffer itself.
|
// the buffer itself.
|
||||||
evbuffer* buffer = nullptr;
|
evbuffer* buffer = nullptr;
|
||||||
|
|
||||||
|
// IP protocol to use when making the request
|
||||||
|
IPProtocol ip_proto = IPProtocol::ANY;
|
||||||
|
|
||||||
static constexpr int DefaultTimeoutSecs = 120;
|
static constexpr int DefaultTimeoutSecs = 120;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue