Files
2026-04-09 16:42:44 +02:00

2101 lines
61 KiB
C++

/*
Copyright (c) 2015-2022, Arvid Norberg
Copyright (c) 2016, Alden Torres
Copyright (c) 2017-2018, Steven Siloti
All rights reserved.
You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/
#include "test.hpp"
#include "settings.hpp"
#include "setup_swarm.hpp"
#include "setup_transfer.hpp" // for addr()
#include "utils.hpp" // for print_alerts
#include "create_torrent.hpp"
#include "simulator/simulator.hpp"
#include "simulator/http_server.hpp"
#include "simulator/utils.hpp"
#include "libtorrent/alert_types.hpp"
#include "libtorrent/announce_entry.hpp"
#include "libtorrent/session.hpp"
#include "libtorrent/create_torrent.hpp"
#include "libtorrent/file_storage.hpp"
#include "libtorrent/torrent_info.hpp"
#include "libtorrent/load_torrent.hpp"
#include "libtorrent/aux_/ip_helpers.hpp" // for is_v4
#include <boost/optional.hpp>
#include <iostream>
using namespace lt;
using namespace sim;
using chrono::duration_cast;
// seconds
const int duration = 10000;
template <typename Tp1, typename Tp2>
bool eq(Tp1 const lhs, Tp2 const rhs)
{
return std::abs(lt::duration_cast<seconds>(lhs - rhs).count()) <= 1;
}
void test_interval(int interval)
{
using sim::asio::ip::address_v4;
sim::default_config network_cfg;
sim::simulation sim{network_cfg};
bool ran_to_completion = false;
sim::asio::io_context web_server(sim, make_address_v4("2.2.2.2"));
// listen on port 8080
sim::http_server http(web_server, 8080);
// the timestamps of all announces
std::vector<lt::time_point> announces;
http.register_handler("/announce"
, [&announces,interval,&ran_to_completion](std::string /* method */
, std::string /* req */
, std::map<std::string, std::string>&)
{
// don't collect events once we're done. We're not interested in the
// tracker stopped announce for instance
if (!ran_to_completion)
announces.push_back(lt::clock_type::now());
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali%de5:peers0:e", interval);
return sim::send_response(200, "OK", size) + response;
});
std::vector<lt::time_point> announce_alerts;
lt::settings_pack default_settings = settings();
// since the test tracker is only listening on IPv4 we need to configure the
// client to do the same so that the number of tracker_announce_alerts matches
// the number of announces seen by the tracker
default_settings.set_str(settings_pack::listen_interfaces, "0.0.0.0:6881");
lt::add_torrent_params default_add_torrent;
setup_swarm(1, swarm_test::upload, sim, default_settings, default_add_torrent
// add session
, [](lt::settings_pack&) {}
// add torrent
, [](lt::add_torrent_params& params) {
params.trackers.push_back("http://2.2.2.2:8080/announce");
}
// on alert
, [&](lt::alert const* a, lt::session&) {
if (ran_to_completion) return;
if (lt::alert_cast<lt::tracker_announce_alert>(a))
{
announce_alerts.push_back(a->timestamp());
}
}
// terminate
, [&](int const ticks, lt::session&) -> bool {
if (ticks > duration + 1)
{
ran_to_completion = true;
return true;
}
return false;
});
TEST_CHECK(ran_to_completion);
TEST_EQUAL(announce_alerts.size(), announces.size());
TEST_CHECK(announces.size() % 2 == 0);
lt::time_point last_announce = announces[0];
lt::time_point last_alert = announce_alerts[0];
for (int i = 2; i < int(announces.size()); i += 2)
{
// make sure the interval is within 1 second of what it's supposed to be
// (this accounts for network latencies, and the second-granularity
// timestamps)
TEST_CHECK(eq(duration_cast<lt::seconds>(announces[i] - last_announce), lt::seconds(interval)));
last_announce = announces[i];
TEST_CHECK(eq(duration_cast<lt::milliseconds>(announce_alerts[i] - last_alert), lt::seconds(interval)));
last_alert = announce_alerts[i];
}
}
template <typename AddTorrent, typename OnAlert>
std::vector<std::string> test_event(swarm_test_t const type
, AddTorrent add_torrent
, OnAlert on_alert)
{
using sim::asio::ip::address_v4;
sim::default_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context web_server(sim, make_address_v4("2.2.2.2"));
// listen on port 8080
sim::http_server http(web_server, 8080);
// the request strings of all announces
std::vector<std::string> announces;
const int interval = 500;
http.register_handler("/announce"
, [&](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
announces.push_back(req);
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali%de5:peers0:e", interval);
return sim::send_response(200, "OK", size) + response;
});
lt::settings_pack default_settings = settings();
lt::add_torrent_params default_add_torrent;
setup_swarm(2, type, sim, default_settings, default_add_torrent
// add session
, [](lt::settings_pack&) { }
// add torrent
, add_torrent
// on alert
, on_alert
// terminate
, [&](int const ticks, lt::session& ses) -> bool
{
return ticks > duration;
});
// this is some basic sanity checking of the announces that should always be
// true.
// the first announce should be event=started then no other announce should
// have event=started.
// only the last announce should have event=stopped.
TEST_CHECK(announces.size() > 2);
// to keep things simple, just consider one of the v1 or v2 announces, since
// we use a hybrid torrent, we get double announces.
std::map<std::string, std::vector<std::string>> announces_ih;
for (auto&& a : announces)
{
auto const ih = a.find("info_hash=");
TEST_CHECK(ih != std::string::npos);
auto const key = a.substr(ih, 20);
announces_ih[key].push_back(std::move(a));
}
for (auto const& entry : announces_ih)
{
auto const& ann = entry.second;
TEST_CHECK(ann.size() > 2);
TEST_CHECK(ann.front().find("&event=started") != std::string::npos);
for (auto const& a : span<std::string const>(ann).subspan(1))
TEST_CHECK(a.find("&event=started") == std::string::npos);
TEST_CHECK(ann.back().find("&event=stopped") != std::string::npos);
for (auto const& a : span<std::string const>(ann).first(ann.size() - 1))
TEST_CHECK(a.find("&event=stopped") == std::string::npos);
}
return announces_ih.begin()->second;
}
TORRENT_TEST(event_completed_downloading)
{
auto const announces = test_event(swarm_test::download
, [](lt::add_torrent_params& params) {
params.trackers.push_back("http://2.2.2.2:8080/announce");
}
, [&](lt::alert const*, lt::session&) {}
);
// make sure there's exactly one event=completed
TEST_CHECK(std::count_if(announces.begin(), announces.end(), [](std::string const& s)
{ return s.find("&event=completed") != std::string::npos; }) == 1);
}
TORRENT_TEST(event_completed_downloading_replace_trackers)
{
auto const announces = test_event(swarm_test::download
, [](lt::add_torrent_params& params) {}
, [&](lt::alert const* a, lt::session&) {
if (auto const* at = alert_cast<add_torrent_alert>(a))
at->handle.replace_trackers({announce_entry{"http://2.2.2.2:8080/announce"}});
}
);
// make sure there's exactly one event=completed
TEST_CHECK(std::count_if(announces.begin(), announces.end(), [](std::string const& s)
{ return s.find("&event=completed") != std::string::npos; }) == 1);
}
TORRENT_TEST(event_completed_seeding)
{
auto const announces = test_event(swarm_test::upload | swarm_test::no_auto_stop
, [](lt::add_torrent_params& params) {
params.trackers.push_back("http://2.2.2.2:8080/announce");
}
, [&](lt::alert const*, lt::session&) {}
);
// make sure there are no event=completed, since we added the torrent as a
// seed
TEST_CHECK(std::count_if(announces.begin(), announces.end(), [](std::string const& s)
{ return s.find("&event=completed") != std::string::npos; }) == 0);
}
TORRENT_TEST(event_completed_seeding_replace_trackers)
{
auto const announces = test_event(swarm_test::upload | swarm_test::no_auto_stop
, [](lt::add_torrent_params& params) {}
, [&](lt::alert const* a, lt::session&) {
if (auto const* at = alert_cast<add_torrent_alert>(a))
at->handle.replace_trackers({announce_entry{"http://2.2.2.2:8080/announce"}});
}
);
// make sure there are no event=completed, since we added the torrent as a
// seed
TEST_CHECK(std::count_if(announces.begin(), announces.end(), [](std::string const& s)
{ return s.find("&event=completed") != std::string::npos; }) == 0);
}
TORRENT_TEST(announce_interval_440)
{
test_interval(440);
}
TORRENT_TEST(announce_interval_1800)
{
test_interval(1800);
}
TORRENT_TEST(announce_interval_1200)
{
test_interval(3600);
}
namespace {
struct sim_config : sim::default_config
{
explicit sim_config(bool ipv6 = true) : ipv6(ipv6) {}
chrono::high_resolution_clock::duration hostname_lookup(
asio::ip::address const& requestor
, std::string hostname
, std::vector<asio::ip::address>& result
, boost::system::error_code& ec) override
{
if (hostname == "tracker.com")
{
result.push_back(make_address_v4("123.0.0.2"));
if (ipv6)
result.push_back(make_address_v6("ff::dead:beef"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
if (hostname == "localhost")
{
result.push_back(make_address_v4("127.0.0.1"));
if (ipv6)
result.push_back(make_address_v6("::1"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(1));
}
if (hostname == "xn--tracker-.com")
{
result.push_back(make_address_v4("123.0.0.2"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
if (hostname == "redirector.com")
{
result.push_back(make_address_v4("123.0.0.4"));
return duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
return default_config::hostname_lookup(requestor, hostname, result, ec);
}
bool ipv6;
};
} // anonymous namespace
void on_alert_notify(lt::session* ses)
{
post(ses->get_context(), [ses] {
std::vector<lt::alert*> alerts;
ses->pop_alerts(&alerts);
for (lt::alert* a : alerts)
{
lt::time_duration d = a->timestamp().time_since_epoch();
std::uint32_t const millis = std::uint32_t(
lt::duration_cast<lt::milliseconds>(d).count());
std::printf("%4u.%03u: %s\n", millis / 1000, millis % 1000,
a->message().c_str());
}
});
}
void test_announce()
{
using sim::asio::ip::address_v4;
sim::default_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context web_server(sim, make_address_v4("2.2.2.2"));
// listen on port 8080
sim::http_server http(web_server, 8080);
int announces = 0;
// expect announced IP & port
std::string const expect_port = "&port=1234";
std::string const expect_ip = "&ip=1.2.3.4";
http.register_handler("/announce"
, [&announces, expect_port, expect_ip](std::string method, std::string req
, std::map<std::string, std::string>&)
{
++announces;
TEST_EQUAL(method, "GET");
TEST_CHECK(req.find(expect_port) != std::string::npos);
TEST_CHECK(req.find(expect_ip) != std::string::npos);
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali1800e5:peers0:e");
return sim::send_response(200, "OK", size) + response;
});
{
lt::session_proxy zombie;
std::vector<asio::ip::address> ips;
ips.push_back(make_address("123.0.0.3"));
asio::io_context ios(sim, ips);
lt::settings_pack sett = settings();
sett.set_str(settings_pack::listen_interfaces, "0.0.0.0:6881");
sett.set_str(settings_pack::announce_ip, "1.2.3.4");
sett.set_int(settings_pack::announce_port, 1234);
auto ses = std::make_unique<lt::session>(sett, ios);
ses->set_alert_notify(std::bind(&on_alert_notify, ses.get()));
lt::add_torrent_params p;
p.name = "test-torrent";
p.save_path = ".";
p.info_hashes.v1.assign("abababababababababab");
p.trackers.push_back("http://2.2.2.2:8080/announce");
ses->async_add_torrent(p);
// stop the torrent 5 seconds in
sim::timer t1(sim, lt::seconds(5)
, [&ses](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
for (auto const& t : torrents)
{
t.pause();
}
});
// then shut down 10 seconds in
sim::timer t2(sim, lt::seconds(10)
, [&ses,&zombie](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
}
TEST_EQUAL(announces, 2);
}
// this test makes sure that a seed can overwrite its announced IP & port
TORRENT_TEST(announce_ip_port) {
test_announce();
}
static const int num_interfaces = 3;
void test_ipv6_support(char const* listen_interfaces
, int const expect_v4, int const expect_v6)
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context web_server_v4(sim, make_address_v4("123.0.0.2"));
sim::asio::io_context web_server_v6(sim, make_address_v6("ff::dead:beef"));
// listen on port 8080
sim::http_server http_v4(web_server_v4, 8080);
sim::http_server http_v6(web_server_v6, 8080);
int v4_announces = 0;
int v6_announces = 0;
// if we're not listening we'll just report port 0
std::string const expect_port = (listen_interfaces && listen_interfaces == ""_sv)
? "&port=1" : "&port=6881";
http_v4.register_handler("/announce"
, [&v4_announces,expect_port](std::string method, std::string req
, std::map<std::string, std::string>&)
{
++v4_announces;
TEST_EQUAL(method, "GET");
TEST_CHECK(req.find(expect_port) != std::string::npos);
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali1800e5:peers0:e");
return sim::send_response(200, "OK", size) + response;
});
http_v6.register_handler("/announce"
, [&v6_announces,expect_port](std::string method, std::string req
, std::map<std::string, std::string>&)
{
++v6_announces;
TEST_EQUAL(method, "GET");
TEST_CHECK(req.find(expect_port) != std::string::npos);
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali1800e5:peers0:e");
return sim::send_response(200, "OK", size) + response;
});
{
lt::session_proxy zombie;
std::vector<asio::ip::address> ips;
for (int i = 0; i < num_interfaces; i++)
{
char ep[30];
std::snprintf(ep, sizeof(ep), "123.0.0.%d", i + 1);
ips.push_back(make_address(ep));
std::snprintf(ep, sizeof(ep), "ffff::1337:%d", i + 1);
ips.push_back(make_address(ep));
}
asio::io_context ios(sim, ips);
lt::settings_pack sett = settings();
if (listen_interfaces)
{
sett.set_str(settings_pack::listen_interfaces, listen_interfaces);
}
auto ses = std::make_unique<lt::session>(sett, ios);
ses->set_alert_notify(std::bind(&on_alert_notify, ses.get()));
lt::add_torrent_params p;
p.name = "test-torrent";
p.save_path = ".";
p.info_hashes.v1.assign("abababababababababab");
//TODO: parameterize http vs. udp here
p.trackers.push_back("http://tracker.com:8080/announce");
ses->async_add_torrent(p);
// stop the torrent 5 seconds in
sim::timer t1(sim, lt::seconds(5)
, [&ses](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
for (auto const& t : torrents)
{
t.pause();
}
});
// then shut down 10 seconds in
sim::timer t2(sim, lt::seconds(10)
, [&ses,&zombie](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
}
TEST_EQUAL(v4_announces, expect_v4);
TEST_EQUAL(v6_announces, expect_v6);
}
void test_udpv6_support(char const* listen_interfaces
, int const expect_v4, int const expect_v6)
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context web_server_v4(sim, make_address_v4("123.0.0.2"));
sim::asio::io_context web_server_v6(sim, make_address_v6("ff::dead:beef"));
int v4_announces = 0;
int v6_announces = 0;
{
lt::session_proxy zombie;
std::vector<asio::ip::address> ips;
for (int i = 0; i < num_interfaces; i++)
{
char ep[30];
std::snprintf(ep, sizeof(ep), "123.0.0.%d", i + 1);
ips.push_back(make_address(ep));
std::snprintf(ep, sizeof(ep), "ffff::1337:%d", i + 1);
ips.push_back(make_address(ep));
}
asio::io_context ios(sim, ips);
lt::settings_pack sett = settings();
if (listen_interfaces)
{
sett.set_str(settings_pack::listen_interfaces, listen_interfaces);
}
auto ses = std::make_unique<lt::session>(sett, ios);
// since we don't have a udp tracker to run in the sim, looking for the
// alerts is the closest proxy
ses->set_alert_notify([&]{
post(ses->get_context(), [&] {
std::vector<lt::alert*> alerts;
ses->pop_alerts(&alerts);
for (lt::alert* a : alerts)
{
lt::time_duration d = a->timestamp().time_since_epoch();
std::uint32_t const millis = std::uint32_t(
lt::duration_cast<lt::milliseconds>(d).count());
std::printf("%4u.%03u: %s\n", millis / 1000, millis % 1000,
a->message().c_str());
if (auto tr = alert_cast<tracker_announce_alert>(a))
{
if (lt::aux::is_v4(tr->local_endpoint))
++v4_announces;
else
++v6_announces;
}
else if (alert_cast<tracker_error_alert>(a))
{
TEST_ERROR("unexpected tracker error");
}
}
});
});
lt::add_torrent_params p;
p.name = "test-torrent";
p.save_path = ".";
p.info_hashes.v1.assign("abababababababababab");
p.trackers.push_back("udp://tracker.com:8080/announce");
ses->async_add_torrent(p);
// stop the torrent 5 seconds in
sim::timer t1(sim, lt::seconds(5)
, [&ses](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
for (auto const& t : torrents)
{
t.pause();
}
});
// then shut down 10 seconds in
sim::timer t2(sim, lt::seconds(10)
, [&ses,&zombie](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
}
TEST_EQUAL(v4_announces, expect_v4);
TEST_EQUAL(v6_announces, expect_v6);
}
// this test makes sure that a tracker whose host name resolves to both IPv6 and
// IPv4 addresses will be announced to twice, once for each address family
TORRENT_TEST(ipv6_support)
{
// null means default
test_ipv6_support(nullptr, num_interfaces * 2, num_interfaces * 2);
}
TORRENT_TEST(announce_no_listen)
{
// if we don't listen on any sockets at all we should not announce to trackers
test_ipv6_support("", 0, 0);
}
TORRENT_TEST(announce_udp_no_listen)
{
// if we don't listen on any sockets at all we should not announce to trackers
test_udpv6_support("", 0, 0);
}
TORRENT_TEST(ipv6_support_bind_v4_v6_any)
{
// 2 because there's one announce on startup and one when shutting down
// IPv6 will send announces for each interface
test_ipv6_support("0.0.0.0:6881,[::0]:6881", num_interfaces * 2, num_interfaces * 2);
}
TORRENT_TEST(ipv6_support_bind_v6_any)
{
test_ipv6_support("[::0]:6881", 0, num_interfaces * 2);
}
TORRENT_TEST(ipv6_support_bind_v4)
{
test_ipv6_support("123.0.0.3:6881", 2, 0);
}
TORRENT_TEST(ipv6_support_bind_v6)
{
test_ipv6_support("[ffff::1337:1]:6881", 0, 2);
}
TORRENT_TEST(ipv6_support_bind_v6_3interfaces)
{
test_ipv6_support("[ffff::1337:1]:6881,[ffff::1337:2]:6881,[ffff::1337:3]:6881", 0, 3 * 2);
}
TORRENT_TEST(ipv6_support_bind_v4_v6)
{
test_ipv6_support("123.0.0.3:6881,[ffff::1337:1]:6881", 2, 2);
}
TORRENT_TEST(ipv6_support_bind_v6_v4)
{
test_ipv6_support("[ffff::1337:1]:6881,123.0.0.3:6881", 2, 2);
}
// this runs a simulation of a torrent with tracker(s), making sure the request
// received by the tracker matches the expectation.
// The Setup function is run first, giving the test an opportunity to add
// trackers to the torrent. It's expected to return the number of seconds to
// wait until test2 is called.
// The Announce function is called on http requests. Test1 is run on the session
// 5 seconds after startup. The tracker is running at 123.0.0.2 (or tracker.com)
// port 8080.
template <typename Setup, typename Announce, typename Test1, typename Test2>
void tracker_test(Setup setup, Announce a, Test1 test1, Test2 test2
, char const* url_path = "/announce"
, char const* redirect = "http://123.0.0.2/announce")
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context tracker_ios(sim, make_address_v4("123.0.0.2"));
sim::asio::io_context tracker_ios6(sim, make_address_v6("ff::dead:beef"));
sim::asio::io_context redirector_ios(sim, make_address_v4("123.0.0.4"));
sim::asio::io_context tracker_lo_ios(sim, make_address_v4("127.0.0.1"));
sim::asio::io_context tracker_lo_ios6(sim, make_address_v6("::1"));
// listen on port 8080
sim::http_server http(tracker_ios, 8080);
sim::http_server http6(tracker_ios6, 8080);
sim::http_server http_lo(tracker_lo_ios, 8080);
sim::http_server http6_lo(tracker_lo_ios6, 8080);
sim::http_server http_redirect(redirector_ios, 8080);
http.register_handler(url_path, a);
http6.register_handler(url_path, a);
http_lo.register_handler(url_path, a);
http6_lo.register_handler(url_path, a);
http_redirect.register_redirect(url_path, redirect);
lt::session_proxy zombie;
asio::io_context ios(sim, { make_address_v4("123.0.0.3")
, make_address_v6("ffff::1337") });
lt::settings_pack sett = settings();
auto ses = std::make_unique<lt::session>(sett, ios);
ses->set_alert_notify(std::bind(&on_alert_notify, ses.get()));
lt::add_torrent_params p;
p.info_hashes.v1.assign("abababababababababab");
int const delay = setup(p, *ses);
p.name = "test-torrent";
p.save_path = ".";
ses->async_add_torrent(p);
// run the test 5 seconds in
sim::timer t1(sim, lt::seconds(5)
, [&ses,&test1](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
TEST_EQUAL(torrents.size(), 1);
torrent_handle h = torrents.front();
test1(h);
});
sim::timer t2(sim, lt::seconds(5 + delay)
, [&ses,&test2](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
TEST_EQUAL(torrents.size(), 1);
torrent_handle h = torrents.front();
test2(h);
});
// then shut down 10 seconds in
sim::timer t3(sim, lt::seconds(10 + delay)
, [&ses,&zombie](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
}
template <typename Announce, typename Test1, typename Test2>
void tracker_test(Announce a, Test1 test1, Test2 test2, char const* url_path = "/announce")
{
tracker_test([](lt::add_torrent_params& p, lt::session&) {
p.trackers.push_back("http://tracker.com:8080/announce");
return 5;
},
a, test1, test2, url_path);
}
template <typename Announce, typename Test>
void announce_entry_test(Announce a, Test t, char const* url_path = "/announce")
{
tracker_test(a
, [&t] (torrent_handle h) {
std::vector<announce_entry> tr = h.trackers();
TEST_EQUAL(tr.size(), 1);
announce_entry const& ae = tr[0];
t(ae);
}
, [](torrent_handle){}
, url_path);
}
// test that we correctly omit announcing an event=stopped to a tracker we never
// managed to send an event=start to
TORRENT_TEST(omit_stop_event)
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
sim::simulation sim{network_cfg};
lt::session_proxy zombie;
asio::io_context ios(sim, { make_address_v4("123.0.0.3"), make_address_v6("ff::dead:beef")});
lt::settings_pack sett = settings();
std::unique_ptr<lt::session> ses(new lt::session(sett, ios));
print_alerts(*ses);
lt::add_torrent_params p;
p.name = "test-torrent";
p.save_path = ".";
p.info_hashes.v1.assign("abababababababababab");
p.trackers.push_back("udp://tracker.com:8080/announce");
ses->async_add_torrent(p);
// run the test 5 seconds in
sim::timer t1(sim, lt::seconds(5)
, [&ses](boost::system::error_code const&)
{
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
TEST_EQUAL(torrents.size(), 1);
torrent_handle h = torrents.front();
});
int stop_announces = 0;
sim::timer t2(sim, lt::seconds(1800)
, [&](boost::system::error_code const&)
{
// make sure we don't announce a stopped event when stopping
print_alerts(*ses, [&](lt::session&, lt::alert const* a) {
if (alert_cast<lt::tracker_announce_alert>(a))
++stop_announces;
});
std::vector<lt::torrent_handle> torrents = ses->get_torrents();
TEST_EQUAL(torrents.size(), 1);
torrent_handle h = torrents.front();
h.set_flags(torrent_flags::paused, torrent_flags::paused | torrent_flags::auto_managed);
});
// then shut down 10 seconds in
sim::timer t3(sim, lt::seconds(1810)
, [&](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
TEST_EQUAL(stop_announces, 0);
}
TORRENT_TEST(test_error)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response), "d14:failure reason4:teste");
return sim::send_response(200, "OK", size) + response;
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "test");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code(errors::tracker_failure));
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 1);
}
});
}
TORRENT_TEST(test_no_announce_path)
{
tracker_test(
[](lt::add_torrent_params& p, lt::session&) {
p.trackers.push_back("http://tracker.com:8080");
return 5;
},
[](std::string method, std::string req, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response), "d5:peers6:aaaaaae");
return sim::send_response(200, "OK", size) + response;
}
, [](torrent_handle h)
{
std::vector<announce_entry> tr = h.trackers();
TEST_EQUAL(tr.size(), 1);
announce_entry const& ae = tr[0];
TEST_EQUAL(ae.url, "http://tracker.com:8080");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code());
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
}
}
, [](torrent_handle){}
, "/");
}
TORRENT_TEST(paused_session)
{
using sim::asio::ip::address_v4;
sim_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context tracker_ios(sim, make_address_v4("123.0.0.2"));
// listen on port 8080
sim::http_server http(tracker_ios, 8080);
int announces = 0;
http.register_handler("/announce",
[&announces](std::string method, std::string req, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
++announces;
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali1800e5:peers6:aaaaaae");
return sim::send_response(200, "OK", size) + response;
}
);
lt::session_proxy zombie;
asio::io_context ios(sim, { make_address_v4("123.0.0.3")
, make_address_v6("ffff::1337") });
lt::settings_pack sett = settings();
auto ses = std::make_unique<lt::session>(sett, ios);
ses->set_alert_notify(std::bind(&on_alert_notify, ses.get()));
lt::add_torrent_params p;
p.name = "test-torrent";
p.save_path = ".";
p.info_hashes.v1.assign("abababababababababab");
p.trackers.push_back("http://123.0.0.2:8080/announce");
ses->async_add_torrent(p);
lt::seconds timeline(5);
// pause the session
sim::timer t1(sim, timeline
, [&announces,&ses](boost::system::error_code const&)
{
// make sure we got 1 announce
TEST_EQUAL(announces, 1);
ses->pause();
});
// wait until the next tracker announce should have happened, but didn't
// because the session is paused
timeline += seconds(1801);
sim::timer t2(sim, timeline
, [&announces,&ses](boost::system::error_code const&)
{
// the stop is announced
TEST_EQUAL(announces, 2);
ses->resume();
});
timeline += seconds(5);
sim::timer t3(sim, timeline
, [&announces](boost::system::error_code const&)
{
// make sure we got another announce
TEST_EQUAL(announces, 3);
});
timeline += seconds(5);
// then shut down
sim::timer t4(sim, timeline
, [&ses,&zombie](boost::system::error_code const&)
{
zombie = ses->abort();
ses.reset();
});
sim.run();
}
TORRENT_TEST(test_warning)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response), "d5:peers6:aaaaaa15:warning message5:test2e");
return sim::send_response(200, "OK", size) + response;
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "test2");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code());
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
}
});
}
TORRENT_TEST(test_scrape_data_in_announce)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response),
"d5:peers6:aaaaaa8:completei1e10:incompletei2e10:downloadedi3e11:downloadersi4ee");
return sim::send_response(200, "OK", size) + response;
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code());
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_complete, 1);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_incomplete, 2);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_downloaded, 3);
}
});
}
TORRENT_TEST(test_scrape)
{
tracker_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response),
"d5:filesd20:ababababababababababd8:completei1e10:downloadedi3e10:incompletei2eeee");
return sim::send_response(200, "OK", size) + response;
}
, [](torrent_handle h)
{
h.scrape_tracker();
}
, [](torrent_handle h)
{
std::vector<announce_entry> tr = h.trackers();
TEST_EQUAL(tr.size(), 1);
announce_entry const& ae = tr[0];
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_incomplete, 2);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_complete, 1);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].scrape_downloaded, 3);
}
}
, "/scrape");
}
TORRENT_TEST(test_http_status)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
return sim::send_response(410, "Not A Tracker", 0);
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "Not A Tracker");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code(410, http_category()));
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 1);
}
});
}
TORRENT_TEST(test_interval)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response)
, "d10:tracker id8:testteste");
return sim::send_response(200, "OK", size) + response;
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code());
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
}
TEST_EQUAL(ae.trackerid, "testtest");
});
}
TORRENT_TEST(test_invalid_bencoding)
{
announce_entry_test(
[](std::string method, std::string req
, std::map<std::string, std::string>&)
{
TEST_EQUAL(method, "GET");
char response[500];
int const size = std::snprintf(response, sizeof(response)
, "d10:tracer idteste");
return sim::send_response(200, "OK", size) + response;
}
, [](announce_entry const& ae)
{
TEST_EQUAL(ae.url, "http://tracker.com:8080/announce");
TEST_EQUAL(ae.endpoints.size(), 2);
for (auto const& aep : ae.endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "");
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error, error_code(bdecode_errors::expected_value
, bdecode_category()));
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 1);
}
});
}
TORRENT_TEST(try_next)
{
// test that we move on to try the next tier if the first one fails
bool got_announce = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session&)
{
// TODO: 3 use tracker_tiers here to put the trackers in different tiers
p.trackers.push_back("udp://failing-tracker.com/announce");
p.trackers.push_back("http://failing-tracker.com/announce");
// this is the working tracker
p.trackers.push_back("http://tracker.com:8080/announce");
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>&)
{
got_announce = true;
TEST_EQUAL(method, "GET");
char response[500];
// respond with an empty peer list
int const size = std::snprintf(response, sizeof(response), "d5:peers0:e");
return sim::send_response(200, "OK", size) + response;
}
, [](torrent_handle h) {}
, [](torrent_handle h)
{
torrent_status st = h.status();
TEST_EQUAL(st.current_tracker, "http://tracker.com:8080/announce");
std::vector<announce_entry> tr = h.trackers();
TEST_EQUAL(tr.size(), 3);
for (int i = 0; i < int(tr.size()); ++i)
{
std::printf("tracker \"%s\"\n", tr[i].url.c_str());
if (tr[i].url == "http://tracker.com:8080/announce")
{
for (auto const& aep : tr[i].endpoints)
{
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
}
TEST_EQUAL(tr[i].verified, true);
}
else if (tr[i].url == "http://failing-tracker.com/announce")
{
for (auto const& aep : tr[i].endpoints)
{
TEST_CHECK(aep.info_hashes[protocol_version::V1].fails >= 1);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error
, error_code(boost::asio::error::host_not_found));
}
TEST_EQUAL(tr[i].verified, false);
}
else if (tr[i].url == "udp://failing-tracker.com/announce")
{
TEST_EQUAL(tr[i].verified, false);
for (auto const& aep : tr[i].endpoints)
{
TEST_CHECK(aep.info_hashes[protocol_version::V1].fails >= 1);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].last_error
, error_code(boost::asio::error::host_not_found));
}
}
else
{
TEST_ERROR(("unexpected tracker URL: " + tr[i].url).c_str());
}
}
});
TEST_EQUAL(got_announce, true);
}
TORRENT_TEST(clear_error)
{
// make sure we clear the error from a previous attempt when succeeding
// a tracker announce
int num_announces = 0;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
// make sure we just listen on a single listen interface
pack.set_str(settings_pack::listen_interfaces, "123.0.0.3:0");
pack.set_int(settings_pack::min_announce_interval, 1);
pack.set_int(settings_pack::tracker_backoff, 1);
ses.apply_settings(pack);
p.trackers.push_back("http://tracker.com:8080/announce");
return 60;
},
[&](std::string method, std::string req, std::map<std::string, std::string>&)
{
// don't count the stopped event when shutting down
if (req.find("&event=stopped&") != std::string::npos)
{
return sim::send_response(200, "OK", 2) + "de";
}
if (num_announces++ == 0)
{
// the first announce fails
return std::string{};
}
// the second announce succeeds, with an empty peer list
char response[500];
int const size = std::snprintf(response, sizeof(response), "d8:intervali1800e5:peers0:e");
return sim::send_response(200, "OK", size) + response;
}
, [](torrent_handle h) {
}
, [&](torrent_handle h)
{
std::vector<announce_entry> const tr = h.trackers();
TEST_EQUAL(tr.size(), 1);
std::printf("tracker \"%s\"\n", tr[0].url.c_str());
TEST_EQUAL(tr[0].url, "http://tracker.com:8080/announce");
TEST_EQUAL(tr[0].endpoints.size(), 1);
auto const& aep = tr[0].endpoints[0];
TEST_EQUAL(aep.info_hashes[protocol_version::V1].fails, 0);
TEST_CHECK(!aep.info_hashes[protocol_version::V1].last_error);
TEST_EQUAL(aep.info_hashes[protocol_version::V1].message, "");
#if TORRENT_ABI_VERSION <= 2
TEST_EQUAL(aep.fails, 0);
TEST_CHECK(!aep.last_error);
TEST_EQUAL(aep.message, "");
#endif
});
TEST_EQUAL(num_announces, 2);
}
lt::add_torrent_params make_torrent(bool priv)
{
std::vector<lt::create_file_entry> fs;
fs.emplace_back("foobar", 13241);
lt::create_torrent ct(std::move(fs));
ct.add_tracker("http://tracker.com:8080/announce");
for (piece_index_t i(0); i < piece_index_t(ct.num_pieces()); ++i)
ct.set_hash(i, sha1_hash::max());
ct.set_priv(priv);
return lt::load_torrent_buffer(lt::bencode(ct.generate()));
}
// make sure we _do_ send our IPv6 address to trackers for private torrents
TORRENT_TEST(tracker_ipv6_argument)
{
bool got_announce = false;
bool got_ipv6 = false;
bool got_ipv4 = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::anonymous_mode, false);
pack.set_str(settings_pack::listen_interfaces, "123.0.0.3:0,[ffff::1337]:0");
ses.apply_settings(pack);
p = make_torrent(true);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>&)
{
got_announce = true;
bool const stop_event = req.find("&event=stopped") != std::string::npos;
// stop events don't need to advertise the IPv6/IPv4 address
{
std::string::size_type const pos = req.find("&ipv6=");
TEST_CHECK(pos != std::string::npos || stop_event);
got_ipv6 |= pos != std::string::npos;
// make sure the IPv6 argument is url encoded
TEST_EQUAL(req.substr(pos + 6, req.substr(pos + 6).find_first_of('&'))
, "ffff%3a%3a1337");
}
{
std::string::size_type const pos = req.find("&ipv4=");
TEST_CHECK(pos != std::string::npos || stop_event);
got_ipv4 |= pos != std::string::npos;
TEST_EQUAL(req.substr(pos + 6, req.substr(pos + 6).find_first_of('&')), "123.0.0.3");
}
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle) {}
, [](torrent_handle) {});
TEST_EQUAL(got_announce, true);
TEST_EQUAL(got_ipv6, true);
}
TORRENT_TEST(tracker_key_argument)
{
std::set<std::string> keys;
tracker_test(
[](lt::add_torrent_params& p, lt::session&)
{
p = make_torrent(true);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string, std::string req
, std::map<std::string, std::string>&)
{
auto const pos = req.find("&key=");
TEST_CHECK(pos != std::string::npos);
keys.insert(req.substr(pos + 5, req.find_first_of('&', pos + 5) - pos - 5));
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {});
// make sure we got the same key for all listen socket interface
TEST_EQUAL(keys.size(), 1);
}
// make sure we do _not_ send our IPv6 address to trackers for non-private
// torrents
TORRENT_TEST(tracker_ipv6_argument_non_private)
{
bool got_announce = false;
bool got_ipv6 = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::anonymous_mode, false);
ses.apply_settings(pack);
p = make_torrent(false);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>&)
{
got_announce = true;
std::string::size_type pos = req.find("&ipv6=");
TEST_CHECK(pos == std::string::npos);
got_ipv6 |= pos != std::string::npos;
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle) {}
, [](torrent_handle) {});
TEST_EQUAL(got_announce, true);
TEST_EQUAL(got_ipv6, false);
}
TORRENT_TEST(tracker_ipv6_argument_privacy_mode)
{
bool got_announce = false;
bool got_ipv6 = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::anonymous_mode, true);
ses.apply_settings(pack);
p = make_torrent(true);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>&)
{
got_announce = true;
std::string::size_type pos = req.find("&ipv6=");
TEST_CHECK(pos == std::string::npos);
got_ipv6 |= pos != std::string::npos;
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle) {}
, [](torrent_handle) {});
TEST_EQUAL(got_announce, true);
TEST_EQUAL(got_ipv6, false);
}
TORRENT_TEST(tracker_user_agent_privacy_mode_public_torrent)
{
bool got_announce = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::anonymous_mode, true);
pack.set_str(settings_pack::user_agent, "test_agent/1.2.3");
ses.apply_settings(pack);
p = make_torrent(false);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>& headers)
{
got_announce = true;
// in anonymous mode we should send a generic user agent
TEST_CHECK(headers["user-agent"] == "curl/7.81.0");
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {});
TEST_EQUAL(got_announce, true);
}
TORRENT_TEST(tracker_user_agent_privacy_mode_private_torrent)
{
bool got_announce = false;
tracker_test(
[](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::anonymous_mode, true);
pack.set_str(settings_pack::user_agent, "test_agent/1.2.3");
ses.apply_settings(pack);
p = make_torrent(true);
p.info_hashes = lt::info_hash_t{};
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>& headers)
{
got_announce = true;
// in anonymous mode we should still send the user agent for private
// torrents (since private trackers sometimes require it)
TEST_CHECK(headers["user-agent"] == "test_agent/1.2.3");
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {});
TEST_EQUAL(got_announce, true);
}
bool test_ssrf(char const* announce_path, bool const feature_on
, char const* tracker_url)
{
bool got_announce = false;
tracker_test(
[&](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::ssrf_mitigation, feature_on);
ses.apply_settings(pack);
p.trackers.emplace_back(tracker_url);
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>& headers)
{
got_announce = true;
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {}
, announce_path);
return got_announce;
}
TORRENT_TEST(ssrf_localhost)
{
TEST_CHECK(test_ssrf("/announce", true, "http://localhost:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://localhost:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://localhost:8080/unusual-announce-path"));
TEST_CHECK(!test_ssrf("/short", true, "http://localhost:8080/short"));
TEST_CHECK(test_ssrf("/short", false, "http://localhost:8080/short"));
}
TORRENT_TEST(ssrf_IPv4)
{
TEST_CHECK(test_ssrf("/announce", true, "http://127.0.0.1:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://127.0.0.1:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://127.0.0.1:8080/unusual-announce-path"));
}
TORRENT_TEST(ssrf_IPv6)
{
TEST_CHECK(test_ssrf("/announce", true, "http://[::1]:8080/announce"));
TEST_CHECK(!test_ssrf("/unusual-announce-path", true, "http://[::1]:8080/unusual-announce-path"));
TEST_CHECK(test_ssrf("/unusual-announce-path", false, "http://[::1]:8080/unusual-announce-path"));
}
TORRENT_TEST(ssrf_query_string)
{
// tracker URLs that come pre-baked with query string arguments will be
// rejected when SSRF-mitigation is enabled
TEST_CHECK(!test_ssrf("/announce", true, "http://tracker.com:8080/announce?info_hash=abc"));
TEST_CHECK(!test_ssrf("/announce", true, "http://tracker.com:8080/announce?iNfo_HaSh=abc"));
TEST_CHECK(!test_ssrf("/announce", true, "http://tracker.com:8080/announce?event=abc"));
TEST_CHECK(!test_ssrf("/announce", true, "http://tracker.com:8080/announce?EvEnT=abc"));
TEST_CHECK(test_ssrf("/announce", false, "http://tracker.com:8080/announce?info_hash=abc"));
TEST_CHECK(test_ssrf("/announce", false, "http://tracker.com:8080/announce?iNfo_HaSh=abc"));
TEST_CHECK(test_ssrf("/announce", false, "http://tracker.com:8080/announce?event=abc"));
}
bool test_idna(char const* tracker_url, char const* redirect
, bool const feature_on)
{
bool got_announce = false;
tracker_test(
[&](lt::add_torrent_params& p, lt::session& ses)
{
settings_pack pack;
pack.set_bool(settings_pack::allow_idna, feature_on);
ses.apply_settings(pack);
p.trackers.emplace_back(tracker_url);
return 60;
},
[&](std::string method, std::string req
, std::map<std::string, std::string>& headers)
{
got_announce = true;
return sim::send_response(200, "OK", 11) + "d5:peers0:e";
}
, [](torrent_handle h) {}
, [](torrent_handle h) {}
, "/announce"
, redirect ? redirect : ""
);
return got_announce;
}
TORRENT_TEST(tracker_idna)
{
TEST_EQUAL(test_idna("http://tracker.com:8080/announce", nullptr, true), true);
TEST_EQUAL(test_idna("http://tracker.com:8080/announce", nullptr, false), true);
TEST_EQUAL(test_idna("http://xn--tracker-.com:8080/announce", nullptr, true), true);
TEST_EQUAL(test_idna("http://xn--tracker-.com:8080/announce", nullptr, false), false);
}
TORRENT_TEST(tracker_idna_redirect)
{
TEST_EQUAL(test_idna("http://redirector.com:8080/announce", "http://xn--tracker-.com:8080/announce", true), true);
TEST_EQUAL(test_idna("http://redirector.com:8080/announce", "http://xn--tracker-.com:8080/announce", false), false);
}
// This test sets up two peers, one seed an one downloader. The downloader has
// two trackers, both in tier 0. The behavior we expect is that it picks one of
// the trackers at random and announces to it. Since both trackers are working,
// it should not announce to the tracker it did not initially pick.
struct tracker_ent
{
std::string url;
int tier;
};
void test_tracker_tiers(lt::settings_pack pack
, std::vector<address> local_addresses
, std::vector<tracker_ent> trackers
, std::function<void(int (&)[7])> test
, boost::optional<std::function<void(int (&)[7])>> test2 = boost::none)
{
using namespace libtorrent;
pack.set_int(settings_pack::alert_mask, alert_category::error
| alert_category::status
| alert_category::torrent_log);
// setup the simulation
struct sim_config : sim::default_config
{
chrono::high_resolution_clock::duration hostname_lookup(
asio::ip::address const& requestor
, std::string hostname
, std::vector<asio::ip::address>& result
, boost::system::error_code& ec)
{
if (hostname == "ipv6-only-tracker.com")
{
result.push_back(addr("f8e0::1"));
}
else if (hostname == "ipv4-only-tracker.com")
{
result.push_back(addr("3.0.0.1"));
}
else if (hostname == "dual-tracker.com")
{
result.push_back(addr("f8e0::2"));
result.push_back(addr("3.0.0.2"));
}
else return default_config::hostname_lookup(requestor, hostname, result, ec);
return lt::duration_cast<chrono::high_resolution_clock::duration>(chrono::milliseconds(100));
}
};
sim_config network_cfg;
sim::simulation sim{network_cfg};
sim::asio::io_context ios0 { sim, local_addresses};
sim::asio::io_context tracker1(sim, addr("3.0.0.1"));
sim::asio::io_context tracker2(sim, addr("3.0.0.2"));
sim::asio::io_context tracker3(sim, addr("3.0.0.3"));
sim::asio::io_context tracker4(sim, addr("3.0.0.4"));
sim::asio::io_context tracker5(sim, addr("f8e0::1"));
sim::asio::io_context tracker6(sim, addr("f8e0::2"));
sim::asio::io_context tracker7(sim, addr("3.0.0.5"));
sim::http_server http1(tracker1, 8080);
sim::http_server http2(tracker2, 8080);
sim::http_server http3(tracker3, 8080);
sim::http_server http4(tracker4, 8080);
sim::http_server http5(tracker5, 8080);
sim::http_server http6(tracker6, 8080);
sim::http_server http7(tracker7, 8080);
int received_announce[7] = {0, 0, 0, 0, 0, 0, 0};
auto const return_no_peers = [&](std::string method, std::string req
, std::map<std::string, std::string>&, int const tracker_index)
{
++received_announce[tracker_index];
std::string const ret = "d8:intervali1800e5:peers0:e";
return sim::send_response(200, "OK", static_cast<int>(ret.size())) + ret;
};
auto const return_404 = [&](std::string method, std::string req
, std::map<std::string, std::string>&, int const tracker_index)
{
++received_announce[tracker_index];
return sim::send_response(404, "Not Found", 0);
};
using namespace std::placeholders;
http1.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 0));
http2.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 1));
http3.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 2));
http4.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 3));
http5.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 4));
http6.register_handler("/announce", std::bind(return_no_peers, _1, _2, _3, 5));
http7.register_handler("/announce", std::bind(return_404, _1, _2, _3, 6));
lt::session_proxy zombie;
// create session
pack.set_str(settings_pack::listen_interfaces, "0.0.0.0:6881,[::]:6881");
auto ses = std::make_shared<lt::session>(pack, ios0);
print_alerts(*ses);
lt::add_torrent_params params = ::create_torrent(1);
params.flags &= ~lt::torrent_flags::auto_managed;
params.flags &= ~lt::torrent_flags::paused;
for (auto const& t : trackers)
{
params.trackers.push_back("http://" + t.url + ":8080/announce");
params.tracker_tiers.push_back(t.tier);
}
params.save_path = save_path(0);
ses->async_add_torrent(params);
sim::timer t(sim, lt::seconds(30), [&](boost::system::error_code const&)
{
test(received_announce);
if (test2)
{
std::memset(&received_announce, 0, sizeof(received_announce));
}
else
{
zombie = ses->abort();
ses.reset();
}
});
if (test2)
{
sim::timer t2(sim, lt::minutes(31), [&](boost::system::error_code const&)
{
(*test2)(received_announce);
zombie = ses->abort();
ses.reset();
});
sim.run();
}
else
{
sim.run();
}
}
bool one_of(int a, int b)
{
return (a == 2 && b == 0) || (a == 0 && b == 2);
}
// the torrent is a hybrid v1 and v2 torrent, so there is one announce per
// info-hash
TORRENT_TEST(tracker_tiers_multi_homed)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_CHECK(one_of(a[0], a[1]));
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
TEST_EQUAL(a[6], 0);
});
}
TORRENT_TEST(tracker_tiers_all_trackers_multi_homed)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
TEST_EQUAL(a[6], 0);
});
}
TORRENT_TEST(tracker_tiers_all_tiers_multi_homed)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_CHECK(one_of(a[0], a[1]));
TEST_CHECK(one_of(a[2], a[3]));
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
TEST_EQUAL(a[6], 0);
});
}
TORRENT_TEST(tracker_tiers_all_trackers_and_tiers_multi_homed)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 2);
TEST_EQUAL(a[3], 2);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_CHECK(one_of(a[0], a[1]));
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers_all_trackers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers_all_tiers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_CHECK(one_of(a[0], a[1]));
TEST_CHECK(one_of(a[2], a[3]));
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers_all_trackers_and_tiers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 2);
TEST_EQUAL(a[3], 2);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
// in this case, we only have an IPv4 address, and the first tracker resolves
// only to an IPv6 address. Make sure we move on to the next one in the tier
TORRENT_TEST(tracker_tiers_unreachable_tracker)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"f8e0::1", 0}, {"3.0.0.2", 0}, {"3.0.0.3", 1}, {"3.0.0.4", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
});
}
// in this test, we have both v6 and v4 connectivity, and we have two trackers
// One is v6 only and one is dual. Since the first tracker was announced to
// using IPv6, the second tracker will *only* be used for IPv4, and not to
// announce IPv6 to again.
TORRENT_TEST(tracker_tiers_v4_and_v6_same_tier)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"ipv6-only-tracker.com", 0}, {"dual-tracker.com", 0}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 2);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers_v4_and_v6_different_tiers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"ipv6-only-tracker.com", 0}, {"dual-tracker.com", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 2);
TEST_EQUAL(a[5], 0);
});
}
// in the same scenario as above, if we announce to all trackers, we expect to
// continue to visit all trackers in the tier, and announce to that additional
// IPv6 address as well
TORRENT_TEST(tracker_tiers_v4_and_v6_all_trackers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"ipv6-only-tracker.com", 0}, {"dual-tracker.com", 0}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 2);
TEST_EQUAL(a[5], 2);
});
}
TORRENT_TEST(tracker_tiers_v4_and_v6_different_tiers_all_trackers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, false);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"ipv6-only-tracker.com", 0}, {"dual-tracker.com", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 2);
TEST_EQUAL(a[5], 0);
});
}
TORRENT_TEST(tracker_tiers_v4_and_v6_different_tiers_all_tiers)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, false);
test_tracker_tiers(pack, { addr("50.0.0.1"), addr("f8e0::10") }
, { {"ipv6-only-tracker.com", 0}, {"dual-tracker.com", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 0);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 2);
TEST_EQUAL(a[5], 2);
});
}
TORRENT_TEST(tracker_tiers_retry_all)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
// the torrent is a hybrid torrent, so it will announce twice, once for v1
// and once for v2
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.5", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 0);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
// the failing tracker is retried 17 seconds later
TEST_EQUAL(a[6], 4);
}, std::function<void (int (&)[7])>([](int (&a)[7]) {
// this is 31 minutes later
// the working tracker is re-announced once, since interval is 1800
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 0);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
// The failing tracker is retried after:
// 72 seconds
// 189 seconds
// 396 seconds
// 711 seconds
// 1166 seconds
// 1783 seconds
// 6 * 2 = 12
TEST_EQUAL(a[6], 12);
}));
}
TORRENT_TEST(tracker_tiers_retry_all_multiple_trackers_per_tier)
{
settings_pack pack = settings();
pack.set_bool(settings_pack::announce_to_all_tiers, true);
pack.set_bool(settings_pack::announce_to_all_trackers, true);
// the torrent is a hybrid torrent, so it will announce twice, once for v1
// and once for v2
test_tracker_tiers(pack, { addr("50.0.0.1") }
, { {"3.0.0.1", 0}, {"3.0.0.5", 1}, {"3.0.0.2", 1}}
, [](int (&a)[7]) {
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
// the failing tracker is retried 17 seconds later
TEST_EQUAL(a[6], 4);
}, std::function<void (int (&)[7])>([](int (&a)[7]) {
// this is 31 minutes later
// the working tracker is re-announced once, since interval is 1800
TEST_EQUAL(a[0], 2);
TEST_EQUAL(a[1], 2);
TEST_EQUAL(a[2], 0);
TEST_EQUAL(a[3], 0);
TEST_EQUAL(a[4], 0);
TEST_EQUAL(a[5], 0);
// The failing tracker is retried after:
// 72 seconds
// 189 seconds
// 396 seconds
// 711 seconds
// 1166 seconds
// 1783 seconds
// 6 * 2 = 12
TEST_EQUAL(a[6], 12);
}));
}
// TODO: test external IP
// TODO: test with different queuing settings
// TODO: test when a torrent transitions from downloading to finished and
// finished to seeding
// TODO: test that left, downloaded and uploaded are reported correctly
// TODO: test scrape