Coverage Report

Created: 2026-06-15 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libtorrent/src/ut_metadata.cpp
Line
Count
Source
1
/*
2
3
Copyright (c) 2009, Andrew Resch
4
Copyright (c) 2007-2022, Arvid Norberg
5
Copyright (c) 2015, Steven Siloti
6
Copyright (c) 2016-2018, 2020-2021, Alden Torres
7
Copyright (c) 2017, Andrei Kurushin
8
Copyright (c) 2017, Pavel Pimenov
9
Copyright (c) 2022, Joris CARRIER
10
All rights reserved.
11
12
You may use, distribute and modify this code under the terms of the BSD license,
13
see LICENSE file.
14
*/
15
16
#ifndef TORRENT_DISABLE_EXTENSIONS
17
18
#include <functional>
19
#include <vector>
20
#include <utility>
21
#include <numeric>
22
#include <cstdio>
23
24
#include "libtorrent/aux_/bt_peer_connection.hpp"
25
#include "libtorrent/peer_connection_handle.hpp"
26
#include "libtorrent/bencode.hpp"
27
#include "libtorrent/aux_/torrent.hpp"
28
#include "libtorrent/torrent_handle.hpp"
29
#include "libtorrent/extensions.hpp"
30
#include "libtorrent/extensions/ut_metadata.hpp"
31
#include "libtorrent/alert_types.hpp"
32
#include "libtorrent/aux_/random.hpp"
33
#include "libtorrent/aux_/io_bytes.hpp"
34
#include "libtorrent/performance_counters.hpp" // for counters
35
#include "libtorrent/aux_/time.hpp"
36
37
#if TORRENT_USE_ASSERTS
38
#include "libtorrent/hasher.hpp"
39
#endif
40
41
namespace libtorrent {
42
namespace {
43
44
  enum
45
  {
46
    // this is the max number of bytes we'll
47
    // queue up in the send buffer. If we exceed this,
48
    // we'll wait another second before checking
49
    // the send buffer size again. So, this may limit
50
    // the rate at which we can server metadata to
51
    // 160 kiB/s
52
    send_buffer_limit = 0x4000 * 10,
53
54
    // this is the max number of requests we'll queue
55
    // up. If we get more requests than this, we'll
56
    // start rejecting them, claiming we don't have
57
    // metadata. If the torrent is greater than 16 MiB,
58
    // we may hit this case (and the client requesting
59
    // doesn't throttle its requests)
60
    max_incoming_requests = 1024,
61
  };
62
63
  enum class msg_t : std::uint8_t
64
  {
65
    request, piece, dont_have
66
  };
67
68
  int div_round_up(int numerator, int denominator)
69
0
  {
70
0
    return (numerator + denominator - 1) / denominator;
71
0
  }
72
73
  struct ut_metadata_peer_plugin;
74
75
  struct ut_metadata_plugin final
76
    : torrent_plugin
77
  {
78
2.42k
    explicit ut_metadata_plugin(aux::torrent& t) : m_torrent(t) {}
79
80
    std::shared_ptr<peer_plugin> new_connection(
81
      peer_connection_handle const& pc) override;
82
83
    span<char const> metadata() const
84
21
    {
85
21
      if (!m_metadata.empty()) return m_metadata;
86
21
      if (!m_torrent.valid_metadata()) return {};
87
88
21
      auto const ret = m_torrent.torrent_file().info_section();
89
90
#ifdef TORRENT_EXPENSIVE_INVARIANT_CHECKS
91
      if (m_torrent.torrent_file().info_hashes().has_v1())
92
      {
93
        TORRENT_ASSERT(hasher(ret).final()
94
          == m_torrent.torrent_file().info_hashes().v1);
95
      }
96
      if (m_torrent.torrent_file().info_hashes().has_v2())
97
      {
98
        TORRENT_ASSERT(hasher256(ret).final()
99
          == m_torrent.torrent_file().info_hashes().v2);
100
      }
101
#endif
102
103
21
      return ret;
104
21
    }
105
106
    bool received_metadata(ut_metadata_peer_plugin& source
107
      , span<char const> buf, int piece, int total_size);
108
109
    // returns a piece of the metadata that
110
    // we should request.
111
    // returns -1 if we should hold off the request
112
    int metadata_request(bool has_metadata);
113
114
    void on_piece_pass(piece_index_t) override
115
522
    {
116
      // if we became a seed, copy the metadata from
117
      // the torrent before it is deallocated
118
522
      if (m_torrent.is_seed())
119
21
        metadata();
120
522
    }
121
122
    void metadata_size(int const size)
123
0
    {
124
0
      if (m_torrent.valid_metadata()) return;
125
0
      if (size <= 0 || size > 4 * 1024 * 1024) return;
126
0
      m_metadata.resize(size);
127
0
      m_requested_metadata.resize(div_round_up(size, 16 * 1024));
128
0
    }
129
130
    // explicitly disallow assignment, to silence msvc warning
131
    ut_metadata_plugin& operator=(ut_metadata_plugin const&) = delete;
132
133
  private:
134
    aux::torrent& m_torrent;
135
136
    // this buffer is filled with the info-section of
137
    // the metadata file while downloading it from
138
    // peers. Once we have metadata, we seed it directly from the
139
    // torrent_info of the underlying torrent
140
    aux::vector<char> m_metadata;
141
142
    struct metadata_piece
143
    {
144
0
      metadata_piece() = default;
145
      int num_requests = 0;
146
      time_point last_request = min_time();
147
      std::weak_ptr<ut_metadata_peer_plugin> source;
148
      bool operator<(metadata_piece const& rhs) const
149
0
      { return num_requests < rhs.num_requests; }
150
    };
151
152
    // this vector keeps track of how many times each metadata
153
    // block has been requested and who we ended up getting it from
154
    // std::numeric_limits<int>::max() means we have the piece
155
    aux::vector<metadata_piece> m_requested_metadata;
156
  };
157
158
159
  struct ut_metadata_peer_plugin final
160
    : peer_plugin, std::enable_shared_from_this<ut_metadata_peer_plugin>
161
  {
162
    friend struct ut_metadata_plugin;
163
164
    ut_metadata_peer_plugin(aux::torrent& t, aux::bt_peer_connection& pc
165
      , ut_metadata_plugin& tp)
166
52
      : m_request_limit(min_time())
167
52
      , m_torrent(t)
168
52
      , m_pc(pc)
169
52
      , m_tp(tp)
170
52
    {}
171
172
    // can add entries to the extension handshake
173
    void add_handshake(entry& h) override
174
0
    {
175
0
      entry& messages = h["m"];
176
0
      messages["ut_metadata"] = 2;
177
0
      if (m_torrent.valid_metadata())
178
0
        h["metadata_size"] = m_tp.metadata().size();
179
0
    }
180
181
    // called when the extension handshake from the other end is received
182
    bool on_extension_handshake(bdecode_node const& h) override
183
0
    {
184
0
      m_message_index = 0;
185
0
      if (h.type() != bdecode_node::dict_t) return false;
186
0
      bdecode_node const messages = h.dict_find_dict("m");
187
0
      if (!messages) return false;
188
189
0
      int index = int(messages.dict_find_int_value("ut_metadata", -1));
190
0
      if (index == -1) return false;
191
0
      m_message_index = index;
192
193
0
      int metadata_size = int(h.dict_find_int_value("metadata_size"));
194
0
      if (metadata_size > 0)
195
0
        m_tp.metadata_size(metadata_size);
196
0
      else
197
0
        m_pc.set_has_metadata(false);
198
199
0
      maybe_send_request();
200
0
      return true;
201
0
    }
202
203
    void write_metadata_packet(msg_t const type, int const piece)
204
0
    {
205
0
      TORRENT_ASSERT(!m_pc.associated_torrent().expired());
206
207
#ifndef TORRENT_DISABLE_LOGGING
208
      static char const* names[] = {"request", "data", "dont-have"};
209
      char const* n = "";
210
      if (type >= msg_t::request && type <= msg_t::dont_have) n = names[static_cast<int>(type)];
211
      m_pc.peer_log(peer_log_alert::outgoing_message, peer_log_alert::ut_metadata
212
        , "type: %d (%s) piece: %d", static_cast<int>(type), n, piece);
213
#endif
214
215
      // abort if the peer doesn't support the metadata extension
216
0
      if (m_message_index == 0) return;
217
218
0
      entry e;
219
0
      e["msg_type"] = static_cast<int>(type);
220
0
      e["piece"] = piece;
221
222
0
      char const* metadata = nullptr;
223
0
      int metadata_piece_size = 0;
224
225
0
      if (m_torrent.valid_metadata())
226
0
        e["total_size"] = m_tp.metadata().size();
227
228
0
      if (type == msg_t::piece)
229
0
      {
230
0
        TORRENT_ASSERT(piece >= 0 && piece < (m_tp.metadata().size() + 16 * 1024 - 1) / (16 * 1024));
231
0
        TORRENT_ASSERT(m_pc.associated_torrent().lock()->valid_metadata());
232
0
        TORRENT_ASSERT(m_torrent.valid_metadata());
233
234
0
        int const offset = piece * 16 * 1024;
235
0
        metadata = m_tp.metadata().data() + offset;
236
0
        metadata_piece_size = std::min(
237
0
          int(m_tp.metadata().size()) - offset, 16 * 1024);
238
0
        TORRENT_ASSERT(metadata_piece_size > 0);
239
0
        TORRENT_ASSERT(offset >= 0);
240
0
        TORRENT_ASSERT(offset + metadata_piece_size <= m_tp.metadata().size());
241
0
      }
242
243
      // TODO: 3 use the aux::write_* functions and the span here instead, it
244
      // will fit better with send_buffer()
245
0
      char msg[200];
246
0
      char* header = msg;
247
0
      char* p = &msg[6];
248
0
      int const len = bencode(p, e);
249
0
      int const total_size = 2 + len + metadata_piece_size;
250
0
      namespace io = aux;
251
0
      io::write_uint32(total_size, header);
252
0
      io::write_uint8(aux::bt_peer_connection::msg_extended, header);
253
0
      io::write_uint8(m_message_index, header);
254
255
0
      m_pc.send_buffer({msg, len + 6});
256
      // TODO: we really need to increment the refcounter on the torrent
257
      // while this buffer is still in the peer's send buffer
258
0
      if (metadata_piece_size)
259
0
      {
260
0
        m_pc.append_const_send_buffer(
261
0
          span<char>(const_cast<char*>(metadata), metadata_piece_size), metadata_piece_size);
262
0
      }
263
264
0
      m_pc.stats_counters().inc_stats_counter(counters::num_outgoing_extended);
265
0
      m_pc.stats_counters().inc_stats_counter(counters::num_outgoing_metadata);
266
0
    }
267
268
    bool on_extended(int const length
269
      , int const extended_msg, span<char const> body) override
270
0
    {
271
0
      if (extended_msg != 2) return false;
272
0
      if (m_message_index == 0) return false;
273
274
0
      if (length > 17 * 1024)
275
0
      {
276
#ifndef TORRENT_DISABLE_LOGGING
277
        m_pc.peer_log(peer_log_alert::incoming_message, peer_log_alert::ut_metadata
278
          , "packet too big %d", length);
279
#endif
280
0
        m_pc.disconnect(errors::invalid_metadata_message, operation_t::bittorrent, peer_connection_interface::peer_error);
281
0
        return true;
282
0
      }
283
284
0
      if (!m_pc.packet_finished()) return true;
285
286
0
      error_code ec;
287
0
      bdecode_node msg = bdecode(body, ec);
288
0
      if (msg.type() != bdecode_node::dict_t)
289
0
      {
290
#ifndef TORRENT_DISABLE_LOGGING
291
        m_pc.peer_log(peer_log_alert::incoming_message, peer_log_alert::ut_metadata
292
          , "not a dictionary");
293
#endif
294
0
        m_pc.disconnect(errors::invalid_metadata_message, operation_t::bittorrent, peer_connection_interface::peer_error);
295
0
        return true;
296
0
      }
297
298
0
      bdecode_node const& type_ent = msg.dict_find_int("msg_type");
299
0
      bdecode_node const& piece_ent = msg.dict_find_int("piece");
300
0
      if (!type_ent || !piece_ent)
301
0
      {
302
#ifndef TORRENT_DISABLE_LOGGING
303
        m_pc.peer_log(peer_log_alert::incoming_message, peer_log_alert::ut_metadata
304
          , "missing or invalid keys");
305
#endif
306
0
        m_pc.disconnect(errors::invalid_metadata_message, operation_t::bittorrent, peer_connection_interface::peer_error);
307
0
        return true;
308
0
      }
309
0
      auto const type = msg_t(type_ent.int_value());
310
0
      auto const piece = static_cast<int>(piece_ent.int_value());
311
312
#ifndef TORRENT_DISABLE_LOGGING
313
      m_pc.peer_log(peer_log_alert::incoming_message, peer_log_alert::ut_metadata
314
        , "type: %d piece: %d", static_cast<int>(type), piece);
315
#endif
316
317
0
      switch (type)
318
0
      {
319
0
        case msg_t::request:
320
0
        {
321
0
          if (!m_torrent.valid_metadata()
322
0
            || piece < 0 || piece >= (m_tp.metadata().size() + 16 * 1024 - 1) / (16 * 1024))
323
0
          {
324
#ifndef TORRENT_DISABLE_LOGGING
325
            if (m_pc.should_log(peer_log_alert::info))
326
            {
327
              m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
328
                , "have: %d invalid piece %d metadata size: %d"
329
                , int(m_torrent.valid_metadata()), piece
330
                , m_torrent.valid_metadata()
331
                  ? int(m_tp.metadata().size()) : 0);
332
            }
333
#endif
334
0
            write_metadata_packet(msg_t::dont_have, piece);
335
0
            return true;
336
0
          }
337
0
          if (m_pc.send_buffer_size() < send_buffer_limit)
338
0
            write_metadata_packet(msg_t::piece, piece);
339
0
          else if (m_incoming_requests.size() < max_incoming_requests)
340
0
            m_incoming_requests.push_back(piece);
341
0
          else
342
0
            write_metadata_packet(msg_t::dont_have, piece);
343
0
        }
344
0
        break;
345
0
        case msg_t::piece:
346
0
        {
347
0
          auto const i = std::find(m_sent_requests.begin()
348
0
            , m_sent_requests.end(), piece);
349
350
          // unwanted piece?
351
0
          if (i == m_sent_requests.end())
352
0
          {
353
#ifndef TORRENT_DISABLE_LOGGING
354
            m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
355
              , "UNWANTED / TIMED OUT");
356
#endif
357
0
            return true;
358
0
          }
359
360
0
          m_sent_requests.erase(i);
361
0
          auto const len = msg.data_section().size();
362
0
          auto const total_size = msg.dict_find_int_value("total_size", 0);
363
0
          m_tp.received_metadata(*this, body.subspan(len), piece, static_cast<int>(total_size));
364
0
          maybe_send_request();
365
0
        }
366
0
        break;
367
0
        case msg_t::dont_have:
368
0
        {
369
0
          m_request_limit = std::max(aux::time_now() + minutes(1), m_request_limit);
370
0
          auto const i = std::find(m_sent_requests.begin()
371
0
            , m_sent_requests.end(), piece);
372
          // unwanted piece?
373
0
          if (i == m_sent_requests.end()) return true;
374
0
          m_sent_requests.erase(i);
375
0
        }
376
0
        break;
377
0
      }
378
379
0
      m_pc.stats_counters().inc_stats_counter(counters::num_incoming_metadata);
380
381
0
      return true;
382
0
    }
383
384
    void tick() override
385
0
    {
386
0
      maybe_send_request();
387
0
      while (!m_incoming_requests.empty()
388
0
        && m_pc.send_buffer_size() < send_buffer_limit)
389
0
      {
390
0
        int const piece = m_incoming_requests.front();
391
0
        m_incoming_requests.erase(m_incoming_requests.begin());
392
0
        write_metadata_packet(msg_t::piece, piece);
393
0
      }
394
0
    }
395
396
    void maybe_send_request()
397
0
    {
398
0
      if (m_pc.is_disconnecting()) return;
399
400
      // if we don't have any metadata, and this peer
401
      // supports the request metadata extension
402
      // and we aren't currently waiting for a request
403
      // reply. Then, send a request for some metadata.
404
0
      if (!m_torrent.valid_metadata()
405
0
        && m_message_index != 0
406
0
        && m_sent_requests.size() < 2
407
0
        && has_metadata())
408
0
      {
409
0
        int const piece = m_tp.metadata_request(m_pc.has_metadata());
410
0
        if (piece == -1) return;
411
412
0
        m_sent_requests.push_back(piece);
413
0
        write_metadata_packet(msg_t::request, piece);
414
0
      }
415
0
    }
416
417
    bool has_metadata() const
418
0
    {
419
0
      return m_pc.has_metadata() || (aux::time_now() > m_request_limit);
420
0
    }
421
422
    void failed_hash_check(time_point const& now)
423
0
    {
424
0
      m_request_limit = now + seconds(20 + aux::random(50));
425
0
    }
426
427
    // explicitly disallow assignment, to silence msvc warning
428
    ut_metadata_peer_plugin& operator=(ut_metadata_peer_plugin const&) = delete;
429
430
  private:
431
432
    // this is the message index the remote peer uses
433
    // for metadata extension messages.
434
    int m_message_index = 0;
435
436
    // this is set to the next time we can request pieces
437
    // again. It is updated every time we get a
438
    // "I don't have metadata" message, but also when
439
    // we receive metadata that fails the info hash check
440
    time_point m_request_limit;
441
442
    // request queues
443
    std::vector<int> m_sent_requests;
444
    std::vector<int> m_incoming_requests;
445
446
    aux::torrent& m_torrent;
447
    aux::bt_peer_connection& m_pc;
448
    ut_metadata_plugin& m_tp;
449
  };
450
451
  std::shared_ptr<peer_plugin> ut_metadata_plugin::new_connection(
452
    peer_connection_handle const& pc)
453
429
  {
454
429
    if (pc.type() != connection_type::bittorrent) return {};
455
456
52
    aux::bt_peer_connection* c = static_cast<aux::bt_peer_connection*>(pc.native_handle().get());
457
52
    return std::make_shared<ut_metadata_peer_plugin>(m_torrent, *c, *this);
458
429
  }
459
460
  // has_metadata is false if the peer making the request has not announced
461
  // that it has metadata. In this case, it shouldn't prevent other peers
462
  // from requesting this block by setting a timeout on it.
463
  int ut_metadata_plugin::metadata_request(bool const has_metadata)
464
0
  {
465
0
    auto i = std::min_element(
466
0
      m_requested_metadata.begin(), m_requested_metadata.end());
467
468
0
    if (m_requested_metadata.empty())
469
0
    {
470
      // if we don't know how many pieces there are
471
      // just ask for piece 0
472
0
      m_requested_metadata.resize(1);
473
0
      i = m_requested_metadata.begin();
474
0
    }
475
476
0
    int const piece = int(i - m_requested_metadata.begin());
477
478
    // don't request the same block more than once every 3 seconds
479
    // unless the source is disconnected
480
0
    auto source = m_requested_metadata[piece].source.lock();
481
0
    time_point const now = aux::time_now();
482
0
    if (m_requested_metadata[piece].last_request != min_time()
483
0
      && source
484
0
      && !source->m_pc.is_disconnecting()
485
0
      && total_seconds(now - m_requested_metadata[piece].last_request) < 3)
486
0
      return -1;
487
488
0
    ++m_requested_metadata[piece].num_requests;
489
490
    // only set the timeout on this block, only if the peer
491
    // has metadata. This is to prevent peers with no metadata
492
    // to starve out sending requests to peers with metadata
493
0
    if (has_metadata)
494
0
      m_requested_metadata[piece].last_request = now;
495
496
0
    return piece;
497
0
  }
498
499
  bool ut_metadata_plugin::received_metadata(ut_metadata_peer_plugin& source
500
    , span<char const> buf, int const piece, int const total_size)
501
0
  {
502
0
    if (m_torrent.valid_metadata())
503
0
    {
504
#ifndef TORRENT_DISABLE_LOGGING
505
      source.m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
506
        , "already have metadata");
507
#endif
508
0
      m_torrent.add_redundant_bytes(static_cast<int>(buf.size()), aux::waste_reason::piece_unknown);
509
0
      return false;
510
0
    }
511
512
0
    if (m_metadata.empty())
513
0
    {
514
      // verify the total_size
515
0
      if (total_size <= 0 || total_size > m_torrent.session().settings().get_int(settings_pack::max_metadata_size))
516
0
      {
517
#ifndef TORRENT_DISABLE_LOGGING
518
        source.m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
519
          , "metadata size too big: %d", total_size);
520
#endif
521
// #error post alert
522
0
        return false;
523
0
      }
524
525
0
      m_metadata.resize(total_size);
526
0
      m_requested_metadata.resize(div_round_up(total_size, 16 * 1024));
527
0
    }
528
529
0
    if (piece < 0 || piece >= m_requested_metadata.end_index())
530
0
    {
531
#ifndef TORRENT_DISABLE_LOGGING
532
      source.m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
533
        , "piece: %d INVALID", piece);
534
#endif
535
0
      return false;
536
0
    }
537
538
0
    if (total_size != m_metadata.end_index())
539
0
    {
540
#ifndef TORRENT_DISABLE_LOGGING
541
      source.m_pc.peer_log(peer_log_alert::info, peer_log_alert::ut_metadata
542
        , "total_size: %d INCONSISTENT WITH: %d"
543
        , total_size, int(metadata().size()));
544
#endif
545
      // they disagree about the size!
546
0
      return false;
547
0
    }
548
549
0
    if (piece * 16 * 1024 + buf.size() > metadata().size())
550
0
    {
551
      // this piece is invalid
552
0
      return false;
553
0
    }
554
555
0
    std::memcpy(&m_metadata[piece * 16 * 1024], buf.data(), aux::numeric_cast<std::size_t>(buf.size()));
556
    // mark this piece has 'have'
557
0
    m_requested_metadata[piece].num_requests = std::numeric_limits<int>::max();
558
0
    m_requested_metadata[piece].source = source.shared_from_this();
559
560
0
    bool have_all = std::all_of(m_requested_metadata.begin(), m_requested_metadata.end()
561
0
      , [](metadata_piece const& mp) { return mp.num_requests == std::numeric_limits<int>::max(); });
562
563
0
    if (!have_all) return false;
564
565
0
    if (!m_torrent.set_metadata(m_metadata))
566
0
    {
567
0
      if (!m_torrent.valid_metadata())
568
0
      {
569
0
        time_point const now = aux::time_now();
570
        // any peer that we downloaded metadata from gets a random time
571
        // penalty, from 5 to 30 seconds or so. During this time we don't
572
        // make any metadata requests from those peers (to mix it up a bit
573
        // of which peers we use)
574
        // if we only have one block, and thus requested it from a single
575
        // peer, we bump up the retry time a lot more to try other peers
576
0
        bool single_peer = m_requested_metadata.size() == 1;
577
0
        for (auto& mp : m_requested_metadata)
578
0
        {
579
0
          mp.num_requests = 0;
580
0
          auto peer = mp.source.lock();
581
0
          if (!peer) continue;
582
583
0
          peer->failed_hash_check(single_peer ? now + minutes(5) : now);
584
0
        }
585
0
      }
586
0
      return false;
587
0
    }
588
589
    // free our copy of the metadata and get a reference
590
    // to the torrent's copy instead. No need to keep two
591
    // identical copies around
592
0
    m_metadata.clear();
593
0
    m_metadata.shrink_to_fit();
594
595
    // clear the storage for the bitfield
596
0
    m_requested_metadata.clear();
597
0
    m_requested_metadata.shrink_to_fit();
598
599
0
    return true;
600
0
  }
601
602
} }
603
604
namespace libtorrent {
605
606
  std::shared_ptr<torrent_plugin> create_ut_metadata_plugin(torrent_handle const& th, client_data_t)
607
2.42k
  {
608
2.42k
    aux::torrent* t = th.native_handle().get();
609
    // don't add this extension if the torrent is private
610
2.42k
    if (t->valid_metadata() && t->torrent_file().priv()) return {};
611
2.42k
    return std::make_shared<ut_metadata_plugin>(*t);
612
2.42k
  }
613
}
614
615
#endif