From 3c5442dd7bf46873dc5fa4d4b5ec65bfd417dcc3 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 27 Jan 2022 17:18:50 -0600 Subject: [PATCH] test: add tests for parsing http tracker scrape responses (#2527) --- libtransmission/announcer-common.h | 2 + libtransmission/announcer-http.cc | 180 +++++++++++------------- tests/libtransmission/announcer-test.cc | 154 ++++++++++++++++++++ 3 files changed, 241 insertions(+), 95 deletions(-) diff --git a/libtransmission/announcer-common.h b/libtransmission/announcer-common.h index fd02c08e9..7133b05b1 100644 --- a/libtransmission/announcer-common.h +++ b/libtransmission/announcer-common.h @@ -233,4 +233,6 @@ void tr_tracker_udp_start_shutdown(tr_session* session); void tr_announcerParseHttpAnnounceResponse(tr_announce_response& response, std::string_view msg); +void tr_announcerParseHttpScrapeResponse(tr_scrape_response& response, std::string_view msg); + tr_interned_string tr_announcerGetKey(tr_url_parsed_t const& parsed); diff --git a/libtransmission/announcer-http.cc b/libtransmission/announcer-http.cc index a6357fdf6..9fe46dbb7 100644 --- a/libtransmission/announcer-http.cc +++ b/libtransmission/announcer-http.cc @@ -194,7 +194,7 @@ static void on_announce_done_eventthread(void* vdata) delete data; } -static void maybeLogMessage(std::string_view description, tr_direction direction, std::string_view message) +static void verboseLog(std::string_view description, tr_direction direction, std::string_view message) { auto& out = std::cerr; static bool const verbose = tr_env_key_exists("TR_CURL_VERBOSE"); @@ -222,12 +222,16 @@ static void maybeLogMessage(std::string_view description, tr_direction direction void tr_announcerParseHttpAnnounceResponse(tr_announce_response& response, std::string_view msg) { - maybeLogMessage("Announce response:", TR_DOWN, msg); + verboseLog("Announce response:", TR_DOWN, msg); auto benc = tr_variant{}; auto const variant_loaded = tr_variantFromBuf(&benc, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, msg); + if (!variant_loaded) + { + return; + } - if (variant_loaded && tr_variantIsDict(&benc)) + if (tr_variantIsDict(&benc)) { auto i = int64_t{}; auto sv = std::string_view{}; @@ -288,10 +292,7 @@ void tr_announcerParseHttpAnnounceResponse(tr_announce_response& response, std:: } } - if (variant_loaded) - { - tr_variantFree(&benc); - } + tr_variantFree(&benc); } static void on_announce_done( @@ -375,6 +376,77 @@ static void on_scrape_done_eventthread(void* vdata) delete data; } +void tr_announcerParseHttpScrapeResponse(tr_scrape_response& response, std::string_view msg) +{ + verboseLog("Scrape response:", TR_DOWN, msg); + + auto top = tr_variant{}; + auto const variant_loaded = tr_variantFromBuf(&top, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, msg); + if (!variant_loaded) + { + return; + } + + if (auto sv = std::string_view{}; tr_variantDictFindStrView(&top, TR_KEY_failure_reason, &sv)) + { + response.errmsg = sv; + } + + tr_variant* flags = nullptr; + auto intVal = int64_t{}; + if (tr_variantDictFindDict(&top, TR_KEY_flags, &flags) && + tr_variantDictFindInt(flags, TR_KEY_min_request_interval, &intVal)) + { + response.min_request_interval = intVal; + } + + tr_variant* files = nullptr; + if (tr_variantDictFindDict(&top, TR_KEY_files, &files)) + { + auto key = tr_quark{}; + tr_variant* val = nullptr; + + for (int i = 0; tr_variantDictChild(files, i, &key, &val); ++i) + { + /* populate the corresponding row in our response array */ + for (int j = 0; j < response.row_count; ++j) + { + struct tr_scrape_response_row* row = &response.rows[j]; + + // TODO(ckerr): ugh, interning info dict hashes is awful + auto const& hash = row->info_hash; + auto const key_sv = tr_quark_get_string_view(key); + if (std::size(hash) == std::size(key_sv) && memcmp(std::data(hash), std::data(key_sv), std::size(hash)) == 0) + { + if (tr_variantDictFindInt(val, TR_KEY_complete, &intVal)) + { + row->seeders = intVal; + } + + if (tr_variantDictFindInt(val, TR_KEY_incomplete, &intVal)) + { + row->leechers = intVal; + } + + if (tr_variantDictFindInt(val, TR_KEY_downloaded, &intVal)) + { + row->downloads = intVal; + } + + if (tr_variantDictFindInt(val, TR_KEY_downloaders, &intVal)) + { + row->downloaders = intVal; + } + + break; + } + } + } + } + + tr_variantFree(&top); +} + static void on_scrape_done( tr_session* session, bool did_connect, @@ -385,11 +457,11 @@ static void on_scrape_done( { auto* data = static_cast(vdata); - tr_scrape_response* response = &data->response; - response->did_connect = did_connect; - response->did_timeout = did_timeout; + tr_scrape_response& response = data->response; + response.did_connect = did_connect; + response.did_timeout = did_timeout; - auto const scrape_url_sv = response->scrape_url.sv(); + auto const scrape_url_sv = response.scrape_url.sv(); dbgmsg(data->log_name, "Got scrape response for \"%" TR_PRIsv "\"", TR_PRIsv_ARG(scrape_url_sv)); if (response_code != HTTP_OK) @@ -398,93 +470,11 @@ static void on_scrape_done( char const* response_str = tr_webGetResponseStr(response_code); char buf[512]; tr_snprintf(buf, sizeof(buf), fmt, response_code, response_str); - response->errmsg = buf; + response.errmsg = buf; } else { - auto top = tr_variant{}; - - auto const variant_loaded = tr_variantFromBuf(&top, TR_VARIANT_PARSE_BENC | TR_VARIANT_PARSE_INPLACE, msg); - - if (tr_env_key_exists("TR_CURL_VERBOSE")) - { - if (!variant_loaded) - { - fprintf(stderr, "%s", "Scrape response was not in benc format\n"); - } - else - { - fprintf(stderr, "%s", "Scrape response:\n< "); - for (auto const ch : tr_variantToStr(&top, TR_VARIANT_FMT_JSON)) - { - fputc(ch, stderr); - } - fputc('\n', stderr); - } - } - - if (variant_loaded) - { - if (auto sv = std::string_view{}; tr_variantDictFindStrView(&top, TR_KEY_failure_reason, &sv)) - { - response->errmsg = sv; - } - - tr_variant* flags = nullptr; - auto intVal = int64_t{}; - if (tr_variantDictFindDict(&top, TR_KEY_flags, &flags) && - tr_variantDictFindInt(flags, TR_KEY_min_request_interval, &intVal)) - { - response->min_request_interval = intVal; - } - - tr_variant* files = nullptr; - if (tr_variantDictFindDict(&top, TR_KEY_files, &files)) - { - auto key = tr_quark{}; - tr_variant* val = nullptr; - - for (int i = 0; tr_variantDictChild(files, i, &key, &val); ++i) - { - /* populate the corresponding row in our response array */ - for (int j = 0; j < response->row_count; ++j) - { - struct tr_scrape_response_row* row = &response->rows[j]; - - // TODO(ckerr): ugh, interning info dict hashes is awful - auto const& hash = row->info_hash; - auto const key_sv = tr_quark_get_string_view(key); - if (std::size(hash) == std::size(key_sv) && - memcmp(std::data(hash), std::data(key_sv), std::size(hash)) == 0) - { - if (tr_variantDictFindInt(val, TR_KEY_complete, &intVal)) - { - row->seeders = intVal; - } - - if (tr_variantDictFindInt(val, TR_KEY_incomplete, &intVal)) - { - row->leechers = intVal; - } - - if (tr_variantDictFindInt(val, TR_KEY_downloaded, &intVal)) - { - row->downloads = intVal; - } - - if (tr_variantDictFindInt(val, TR_KEY_downloaders, &intVal)) - { - row->downloaders = intVal; - } - - break; - } - } - } - } - - tr_variantFree(&top); - } + tr_announcerParseHttpScrapeResponse(response, msg); } tr_runInEventThread(session, on_scrape_done_eventthread, data); diff --git a/tests/libtransmission/announcer-test.cc b/tests/libtransmission/announcer-test.cc index 9e603a192..ee03190ec 100644 --- a/tests/libtransmission/announcer-test.cc +++ b/tests/libtransmission/announcer-test.cc @@ -3,6 +3,7 @@ // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. +#include #include #include @@ -146,3 +147,156 @@ TEST_F(AnnouncerTest, parseHttpAnnounceResponseFailureReason) EXPECT_EQ("foobar"sv, response.errmsg); EXPECT_EQ(""sv, response.warning); } + +TEST_F(AnnouncerTest, parseHttpScrapeResponseMulti) +{ + // clang-format off + auto constexpr ResponseBenc = + "d" + "5:files" + "d" + "20:aaaaaaaaaaaaaaaaaaaa" + "d" + "8:complete" "i1e" + "10:incomplete" "i2e" + "10:downloaded" "i3e" + "e" + + "20:bbbbbbbbbbbbbbbbbbbb" + "d" + "8:complete" "i4e" + "10:incomplete" "i5e" + "10:downloaded" "i6e" + "e" + + "20:cccccccccccccccccccc" + "d" + "8:complete" "i7e" + "10:incomplete" "i8e" + "10:downloaded" "i9e" + "e" + "e" + "e"sv; + // clang-format on + + auto response = tr_scrape_response{}; + std::fill_n(std::data(response.rows[0].info_hash), std::size(response.rows[0].info_hash), std::byte{ 'a' }); + std::fill_n(std::data(response.rows[1].info_hash), std::size(response.rows[1].info_hash), std::byte{ 'b' }); + std::fill_n(std::data(response.rows[2].info_hash), std::size(response.rows[2].info_hash), std::byte{ 'c' }); + response.row_count = 3; + tr_announcerParseHttpScrapeResponse(response, ResponseBenc); + + EXPECT_EQ(1, response.rows[0].seeders); + EXPECT_EQ(2, response.rows[0].leechers); + EXPECT_EQ(3, response.rows[0].downloads); + + EXPECT_EQ(4, response.rows[1].seeders); + EXPECT_EQ(5, response.rows[1].leechers); + EXPECT_EQ(6, response.rows[1].downloads); + + EXPECT_EQ(7, response.rows[2].seeders); + EXPECT_EQ(8, response.rows[2].leechers); + EXPECT_EQ(9, response.rows[2].downloads); +} + +TEST_F(AnnouncerTest, parseHttpScrapeResponseMultiWithExcess) +{ + // clang-format off + auto constexpr ResponseBenc = + "d" + "5:files" + "d" + "20:aaaaaaaaaaaaaaaaaaaa" + "d" + "8:complete" "i1e" + "10:incomplete" "i2e" + "10:downloaded" "i3e" + "e" + + "20:bbbbbbbbbbbbbbbbbbbb" + "d" + "8:complete" "i4e" + "10:incomplete" "i5e" + "10:downloaded" "i6e" + "e" + + "20:cccccccccccccccccccc" + "d" + "8:complete" "i7e" + "10:incomplete" "i8e" + "10:downloaded" "i9e" + "e" + + "20:dddddddddddddddddddd" + "d" + "8:complete" "i7e" + "10:incomplete" "i8e" + "10:downloaded" "i9e" + "e" + "e" + "e"sv; + // clang-format on + + auto response = tr_scrape_response{}; + std::fill_n(std::data(response.rows[0].info_hash), std::size(response.rows[0].info_hash), std::byte{ 'a' }); + std::fill_n(std::data(response.rows[1].info_hash), std::size(response.rows[1].info_hash), std::byte{ 'b' }); + std::fill_n(std::data(response.rows[2].info_hash), std::size(response.rows[2].info_hash), std::byte{ 'c' }); + response.row_count = 3; + tr_announcerParseHttpScrapeResponse(response, ResponseBenc); + + EXPECT_EQ(1, response.rows[0].seeders); + EXPECT_EQ(2, response.rows[0].leechers); + EXPECT_EQ(3, response.rows[0].downloads); + + EXPECT_EQ(4, response.rows[1].seeders); + EXPECT_EQ(5, response.rows[1].leechers); + EXPECT_EQ(6, response.rows[1].downloads); + + EXPECT_EQ(7, response.rows[2].seeders); + EXPECT_EQ(8, response.rows[2].leechers); + EXPECT_EQ(9, response.rows[2].downloads); +} + +TEST_F(AnnouncerTest, parseHttpScrapeResponseMultiWithMissing) +{ + // clang-format off + auto constexpr ResponseBenc = + "d" + "5:files" + "d" + "20:aaaaaaaaaaaaaaaaaaaa" + "d" + "8:complete" "i1e" + "10:incomplete" "i2e" + "10:downloaded" "i3e" + "e" + + "20:cccccccccccccccccccc" + "d" + "8:complete" "i7e" + "10:incomplete" "i8e" + "10:downloaded" "i9e" + "e" + "e" + "e"sv; + // clang-format on + + auto response = tr_scrape_response{}; + std::fill_n(std::data(response.rows[0].info_hash), std::size(response.rows[0].info_hash), std::byte{ 'a' }); + std::fill_n(std::data(response.rows[1].info_hash), std::size(response.rows[1].info_hash), std::byte{ 'b' }); + std::fill_n(std::data(response.rows[2].info_hash), std::size(response.rows[2].info_hash), std::byte{ 'c' }); + response.row_count = 3; + tr_announcerParseHttpScrapeResponse(response, ResponseBenc); + + EXPECT_EQ(1, response.rows[0].seeders); + EXPECT_EQ(2, response.rows[0].leechers); + EXPECT_EQ(3, response.rows[0].downloads); + + EXPECT_EQ(0, response.rows[1].seeders); + EXPECT_EQ(0, response.rows[1].leechers); + EXPECT_EQ(0, response.rows[1].downloads); + + EXPECT_EQ(7, response.rows[2].seeders); + EXPECT_EQ(8, response.rows[2].leechers); + EXPECT_EQ(9, response.rows[2].downloads); +}