/* 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 #include using namespace lt; using namespace sim; using chrono::duration_cast; // seconds const int duration = 10000; template bool eq(Tp1 const lhs, Tp2 const rhs) { return std::abs(lt::duration_cast(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 announces; http.register_handler("/announce" , [&announces,interval,&ran_to_completion](std::string /* method */ , std::string /* req */ , std::map&) { // 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 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(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(announces[i] - last_announce), lt::seconds(interval))); last_announce = announces[i]; TEST_CHECK(eq(duration_cast(announce_alerts[i] - last_alert), lt::seconds(interval))); last_alert = announce_alerts[i]; } } template std::vector 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 announces; const int interval = 500; http.register_handler("/announce" , [&](std::string method, std::string req , std::map&) { 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> 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(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(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(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(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& 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::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::milliseconds(1)); } if (hostname == "xn--tracker-.com") { result.push_back(make_address_v4("123.0.0.2")); return duration_cast(chrono::milliseconds(100)); } if (hostname == "redirector.com") { result.push_back(make_address_v4("123.0.0.4")); return duration_cast(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 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(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&) { ++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 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(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 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&) { ++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&) { ++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 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(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 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 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(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 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(d).count()); std::printf("%4u.%03u: %s\n", millis / 1000, millis % 1000, a->message().c_str()); if (auto tr = alert_cast(a)) { if (lt::aux::is_v4(tr->local_endpoint)) ++v4_announces; else ++v6_announces; } else if (alert_cast(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 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 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(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 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 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 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 void announce_entry_test(Announce a, Test t, char const* url_path = "/announce") { tracker_test(a , [&t] (torrent_handle h) { std::vector 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 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 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(a)) ++stop_announces; }); std::vector 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&) { 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&) { 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 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&) { 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(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&) { 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&) { 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&) { 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 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&) { 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&) { 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&) { 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&) { 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 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&) { // 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 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 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&) { 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 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&) { 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&) { 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&) { 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& 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& 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& 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& 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
local_addresses , std::vector trackers , std::function test , boost::optional> 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& 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::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&, int const tracker_index) { ++received_announce[tracker_index]; std::string const ret = "d8:intervali1800e5:peers0:e"; return sim::send_response(200, "OK", static_cast(ret.size())) + ret; }; auto const return_404 = [&](std::string method, std::string req , std::map&, 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(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([](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([](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