Coverage Report

Created: 2025-07-18 06:51

/src/libtorrent/src/http_seed_connection.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
3
Copyright (c) 2008-2021, Arvid Norberg
4
Copyright (c) 2016-2017, Alden Torres
5
Copyright (c) 2016, Andrei Kurushin
6
Copyright (c) 2016, 2018, Steven Siloti
7
Copyright (c) 2017, Pavel Pimenov
8
Copyright (c) 2018, TheOriginalWinCat
9
Copyright (c) 2021, Dmtry Soloviev
10
All rights reserved.
11
12
Redistribution and use in source and binary forms, with or without
13
modification, are permitted provided that the following conditions
14
are met:
15
16
    * Redistributions of source code must retain the above copyright
17
      notice, this list of conditions and the following disclaimer.
18
    * Redistributions in binary form must reproduce the above copyright
19
      notice, this list of conditions and the following disclaimer in
20
      the documentation and/or other materials provided with the distribution.
21
    * Neither the name of the author nor the names of its
22
      contributors may be used to endorse or promote products derived
23
      from this software without specific prior written permission.
24
25
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
29
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
31
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35
POSSIBILITY OF SUCH DAMAGE.
36
37
*/
38
39
#include <cinttypes> // for PRId64 et.al.
40
41
#include "libtorrent/config.hpp"
42
#include "libtorrent/http_seed_connection.hpp"
43
#include "libtorrent/aux_/invariant_check.hpp"
44
#include "libtorrent/aux_/session_impl.hpp"
45
#include "libtorrent/peer_info.hpp"
46
#include "libtorrent/hex.hpp" // for is_hex
47
#include "libtorrent/random.hpp"
48
#include "libtorrent/optional.hpp"
49
50
namespace libtorrent {
51
52
  http_seed_connection::http_seed_connection(peer_connection_args& pack
53
    , web_seed_t& web)
54
0
    : web_connection_base(pack, web)
55
0
    , m_url(web.url)
56
0
    , m_web(&web)
57
0
    , m_response_left(0)
58
0
    , m_chunk_pos(0)
59
0
    , m_partial_chunk_header(0)
60
0
  {
61
0
    INVARIANT_CHECK;
62
63
0
    if (!m_settings.get_bool(settings_pack::report_web_seed_downloads))
64
0
      ignore_stats(true);
65
66
0
    std::shared_ptr<torrent> tor = pack.tor.lock();
67
0
    TORRENT_ASSERT(tor);
68
0
    int blocks_per_piece = tor->torrent_file().piece_length() / tor->block_size();
69
70
    // multiply with the blocks per piece since that many requests are
71
    // merged into one http request
72
0
    max_out_request_queue(m_settings.get_int(settings_pack::urlseed_pipeline_size)
73
0
      * blocks_per_piece);
74
75
0
    prefer_contiguous_blocks(blocks_per_piece);
76
77
#ifndef TORRENT_DISABLE_LOGGING
78
    peer_log(peer_log_alert::info, "CONNECT", "http_seed_connection");
79
#endif
80
0
  }
81
82
  void http_seed_connection::disable(error_code const& ec)
83
0
  {
84
    // we should not try this server again.
85
0
    m_web->disabled = true;
86
0
    disconnect(ec, operation_t::bittorrent, peer_error);
87
0
    if (m_web->ephemeral)
88
0
    {
89
0
      std::shared_ptr<torrent> t = associated_torrent().lock();
90
0
      TORRENT_ASSERT(t);
91
0
      t->remove_web_seed_conn(this);
92
0
    }
93
0
    m_web = nullptr;
94
0
    TORRENT_ASSERT(is_disconnecting());
95
0
  }
96
97
  void http_seed_connection::on_connected()
98
0
  {
99
0
    peer_id pid;
100
0
    aux::random_bytes(pid);
101
0
    set_pid(pid);
102
103
    // this is always a seed
104
0
    incoming_have_all();
105
0
    web_connection_base::on_connected();
106
0
  }
107
108
  void http_seed_connection::disconnect(error_code const& ec
109
    , operation_t const op, disconnect_severity_t const error)
110
0
  {
111
0
    if (is_disconnecting()) return;
112
113
0
    if (op == operation_t::connect && m_web && !m_web->endpoints.empty())
114
0
    {
115
      // we failed to connect to this IP. remove it so that the next attempt
116
      // uses the next IP in the list.
117
0
      m_web->endpoints.erase(m_web->endpoints.begin());
118
0
    }
119
120
0
    std::shared_ptr<torrent> t = associated_torrent().lock();
121
0
    peer_connection::disconnect(ec, op, error);
122
0
    TORRENT_ASSERT(m_web->resolving == false);
123
0
    m_web->peer_info.connection = nullptr;
124
0
  }
125
126
  piece_block_progress http_seed_connection::downloading_piece_progress() const
127
0
  {
128
0
    if (m_requests.empty()) return {};
129
130
0
    std::shared_ptr<torrent> t = associated_torrent().lock();
131
0
    TORRENT_ASSERT(t);
132
133
0
    piece_block_progress ret;
134
135
0
    peer_request const& pr = m_requests.front();
136
0
    ret.piece_index = pr.piece;
137
0
    if (!m_parser.header_finished())
138
0
    {
139
0
      ret.bytes_downloaded = 0;
140
0
    }
141
0
    else
142
0
    {
143
0
      int const receive_buffer_size = int(m_recv_buffer.get().size()) - m_parser.body_start();
144
      // this is an approximation. in chunked encoding mode the chunk headers
145
      // should really be subtracted from the receive_buffer_size
146
0
      ret.bytes_downloaded = std::max(0, t->block_size() - receive_buffer_size);
147
0
    }
148
    // this is used to make sure that the block_index stays within
149
    // bounds. If the entire piece is downloaded, the block_index
150
    // would otherwise point to one past the end
151
0
    int const correction = ret.bytes_downloaded ? -1 : 0;
152
0
    ret.block_index = (pr.start + ret.bytes_downloaded + correction) / t->block_size();
153
0
    ret.full_block_bytes = t->block_size();
154
0
    piece_index_t const last_piece = t->torrent_file().last_piece();
155
0
    if (ret.piece_index == last_piece && ret.block_index
156
0
      == t->torrent_file().piece_size(last_piece) / t->block_size())
157
0
      ret.full_block_bytes = t->torrent_file().piece_size(last_piece) % t->block_size();
158
0
    return ret;
159
0
  }
160
161
  void http_seed_connection::write_request(peer_request const& r)
162
0
  {
163
0
    INVARIANT_CHECK;
164
165
0
    std::shared_ptr<torrent> t = associated_torrent().lock();
166
0
    TORRENT_ASSERT(t);
167
168
0
    TORRENT_ASSERT(t->valid_metadata());
169
    // http_seeds don't support requesting more than one piece
170
    // at a time
171
0
    TORRENT_ASSERT(r.length <= t->torrent_file().piece_size(r.piece));
172
173
0
    std::string request;
174
0
    request.reserve(400);
175
176
0
    int size = r.length;
177
0
    const int bs = t->block_size();
178
0
    const int piece_size = t->torrent_file().piece_length();
179
0
    peer_request pr;
180
0
    while (size > 0)
181
0
    {
182
0
      int request_offset = r.start + r.length - size;
183
0
      pr.start = request_offset % piece_size;
184
0
      pr.length = std::min(bs, size);
185
0
      pr.piece = piece_index_t(static_cast<int>(r.piece) + request_offset / piece_size);
186
0
      m_requests.push_back(pr);
187
0
      size -= pr.length;
188
0
    }
189
190
0
    int proxy_type = m_settings.get_int(settings_pack::proxy_type);
191
0
    bool using_proxy = (proxy_type == settings_pack::http
192
0
      || proxy_type == settings_pack::http_pw) && !m_ssl;
193
194
0
    request += "GET ";
195
0
    request += using_proxy ? m_url : m_path;
196
0
    request += "?info_hash=";
197
0
    request += escape_string({associated_info_hash().data(), 20});
198
0
    request += "&piece=";
199
0
    request += to_string(r.piece);
200
201
    // if we're requesting less than an entire piece we need to
202
    // add ranges
203
0
    if (r.start > 0 || r.length != t->torrent_file().piece_size(r.piece))
204
0
    {
205
0
      request += "&ranges=";
206
0
      request += to_string(r.start).data();
207
0
      request += "-";
208
      // ranges are inclusive, just like HTTP
209
0
      request += to_string(r.start + r.length - 1).data();
210
0
    }
211
212
0
    request += " HTTP/1.1\r\n";
213
0
    add_headers(request, m_settings, using_proxy);
214
0
    request += "\r\n\r\n";
215
0
    m_first_request = false;
216
217
#ifndef TORRENT_DISABLE_LOGGING
218
    peer_log(peer_log_alert::outgoing_message, "REQUEST", "%s", request.c_str());
219
#endif
220
221
0
    send_buffer(request);
222
0
  }
223
224
  // --------------------------
225
  // RECEIVE DATA
226
  // --------------------------
227
228
  void http_seed_connection::on_receive(error_code const& error
229
    , std::size_t bytes_transferred)
230
0
  {
231
0
    INVARIANT_CHECK;
232
233
0
    if (error)
234
0
    {
235
0
      received_bytes(0, int(bytes_transferred));
236
#ifndef TORRENT_DISABLE_LOGGING
237
      if (should_log(peer_log_alert::info))
238
      {
239
        peer_log(peer_log_alert::info, "ERROR"
240
          , "http_seed_connection error: %s", error.message().c_str());
241
      }
242
#endif
243
0
      return;
244
0
    }
245
246
0
    std::shared_ptr<torrent> t = associated_torrent().lock();
247
0
    TORRENT_ASSERT(t);
248
249
0
    for (;;)
250
0
    {
251
0
      span<char const> recv_buffer = m_recv_buffer.get();
252
253
0
      if (bytes_transferred == 0) break;
254
0
      TORRENT_ASSERT(int(recv_buffer.size()) > 0);
255
256
0
      TORRENT_ASSERT(!m_requests.empty());
257
0
      if (m_requests.empty())
258
0
      {
259
0
        received_bytes(0, int(bytes_transferred));
260
0
        disconnect(errors::http_error, operation_t::bittorrent, peer_error);
261
0
        return;
262
0
      }
263
264
0
      peer_request front_request = m_requests.front();
265
266
0
      bool header_finished = m_parser.header_finished();
267
0
      if (!header_finished)
268
0
      {
269
0
        bool parse_error = false;
270
0
        int protocol = 0;
271
0
        int payload = 0;
272
0
        std::tie(payload, protocol) = m_parser.incoming(
273
0
          recv_buffer, parse_error);
274
0
        received_bytes(0, protocol);
275
0
        TORRENT_ASSERT(bytes_transferred >= aux::numeric_cast<std::size_t>(protocol));
276
0
        bytes_transferred -= aux::numeric_cast<std::size_t>(protocol);
277
0
#if TORRENT_USE_ASSERTS
278
0
        if (payload > front_request.length) payload = front_request.length;
279
0
#endif
280
281
0
        if (parse_error)
282
0
        {
283
0
          received_bytes(0, int(bytes_transferred));
284
0
          disconnect(errors::http_parse_error, operation_t::bittorrent, peer_error);
285
0
          return;
286
0
        }
287
288
0
        TORRENT_ASSERT(int(recv_buffer.size()) == 0 || recv_buffer.front() == 'H');
289
290
0
        TORRENT_ASSERT(int(recv_buffer.size()) <= m_recv_buffer.packet_size());
291
292
        // this means the entire status line hasn't been received yet
293
0
        if (m_parser.status_code() == -1)
294
0
        {
295
0
          TORRENT_ASSERT(payload == 0);
296
0
          TORRENT_ASSERT(bytes_transferred == 0);
297
0
          break;
298
0
        }
299
300
        // if the status code is not one of the accepted ones, abort
301
0
        if (!is_ok_status(m_parser.status_code()))
302
0
        {
303
0
          auto const retry_time = value_or(m_parser.header_duration("retry-after")
304
0
            , seconds32(m_settings.get_int(settings_pack::urlseed_wait_retry)));
305
306
          // temporarily unavailable, retry later
307
0
          t->retry_web_seed(this, retry_time);
308
309
0
          if (t->alerts().should_post<url_seed_alert>())
310
0
          {
311
0
            std::string const error_msg = to_string(m_parser.status_code()).data()
312
0
              + (" " + m_parser.message());
313
0
            t->alerts().emplace_alert<url_seed_alert>(t->get_handle(), url()
314
0
              , error_msg);
315
0
          }
316
0
          received_bytes(0, int(bytes_transferred));
317
0
          disconnect(error_code(m_parser.status_code(), http_category()), operation_t::bittorrent, failure);
318
0
          return;
319
0
        }
320
0
        if (!m_parser.header_finished())
321
0
        {
322
0
          TORRENT_ASSERT(payload == 0);
323
0
          TORRENT_ASSERT(bytes_transferred == 0);
324
0
          break;
325
0
        }
326
0
      }
327
328
      // we just completed reading the header
329
0
      if (!header_finished)
330
0
      {
331
0
        if (is_redirect(m_parser.status_code()))
332
0
        {
333
          // this means we got a redirection request
334
          // look for the location header
335
0
          std::string location = m_parser.header("location");
336
0
          received_bytes(0, int(bytes_transferred));
337
338
0
          if (location.empty())
339
0
          {
340
            // we should not try this server again.
341
0
            disable(errors::missing_location);
342
0
            return;
343
0
          }
344
345
          // when SSRF mitigation is enabled, a web seed on the internet (is_global())
346
          // is not allowed to redirect to a server on the local network, so we set
347
          // the no_local_ips flag
348
0
          auto const web_seed_flags = torrent::ephemeral
349
0
            | ((m_settings.get_bool(settings_pack::ssrf_mitigation) && aux::is_global(remote().address()))
350
0
              ? torrent::no_local_ips : web_seed_flag_t{});
351
352
          // add the redirected url and remove the current one
353
0
          t->add_web_seed(location, web_seed_entry::http_seed
354
0
            , std::string{}, web_seed_entry::headers_t{}
355
0
            , web_seed_flags);
356
0
          disable(errors::redirecting);
357
0
          return;
358
0
        }
359
360
0
        std::string const& server_version = m_parser.header("server");
361
0
        if (!server_version.empty())
362
0
          m_server_string = server_version;
363
0
        else
364
0
          m_server_string = m_host;
365
366
0
        m_response_left = atol(m_parser.header("content-length").c_str());
367
0
        if (m_response_left == -1)
368
0
        {
369
0
          received_bytes(0, int(bytes_transferred));
370
          // we should not try this server again.
371
0
          disable(errors::no_content_length);
372
0
          return;
373
0
        }
374
0
        if (m_response_left != front_request.length)
375
0
        {
376
0
          received_bytes(0, int(bytes_transferred));
377
          // we should not try this server again.
378
0
          disable(errors::invalid_range);
379
0
          return;
380
0
        }
381
0
        m_body_start = m_parser.body_start();
382
0
      }
383
384
0
      recv_buffer = recv_buffer.subspan(m_body_start);
385
386
      // =========================
387
      // === CHUNKED ENCODING  ===
388
      // =========================
389
0
      while (m_parser.chunked_encoding()
390
0
        && m_chunk_pos >= 0
391
0
        && m_chunk_pos < recv_buffer.size())
392
0
      {
393
0
        int header_size = 0;
394
0
        std::int64_t chunk_size = 0;
395
0
        span<char const> chunk_start = recv_buffer.subspan(aux::numeric_cast<std::ptrdiff_t>(m_chunk_pos));
396
0
        TORRENT_ASSERT(chunk_start[0] == '\r'
397
0
          || aux::is_hex(chunk_start[0]));
398
0
        bool ret = m_parser.parse_chunk_header(chunk_start, &chunk_size, &header_size);
399
0
        if (!ret)
400
0
        {
401
0
          TORRENT_ASSERT(bytes_transferred >= aux::numeric_cast<std::size_t>(chunk_start.size() - m_partial_chunk_header));
402
0
          bytes_transferred -= aux::numeric_cast<std::size_t>(chunk_start.size() - m_partial_chunk_header);
403
0
          received_bytes(0, aux::numeric_cast<int>(chunk_start.size() - m_partial_chunk_header));
404
0
          m_partial_chunk_header = aux::numeric_cast<int>(chunk_start.size());
405
0
          if (bytes_transferred == 0) return;
406
0
          break;
407
0
        }
408
0
        else
409
0
        {
410
#ifndef TORRENT_DISABLE_LOGGING
411
          peer_log(peer_log_alert::info, "CHUNKED_ENCODING"
412
            , "parsed chunk: %" PRId64 " header_size: %d"
413
            , chunk_size, header_size);
414
#endif
415
0
          TORRENT_ASSERT(bytes_transferred >= aux::numeric_cast<std::size_t>(header_size - m_partial_chunk_header));
416
0
          bytes_transferred -= aux::numeric_cast<std::size_t>(header_size - m_partial_chunk_header);
417
418
0
          received_bytes(0, header_size - m_partial_chunk_header);
419
0
          m_partial_chunk_header = 0;
420
0
          TORRENT_ASSERT(chunk_size != 0 || chunk_start.size() <= header_size || chunk_start[header_size] == 'H');
421
          // cut out the chunk header from the receive buffer
422
0
          TORRENT_ASSERT(m_chunk_pos + m_body_start < INT_MAX);
423
0
          m_recv_buffer.cut(header_size, t->block_size() + 1024, aux::numeric_cast<int>(m_chunk_pos + m_body_start));
424
0
          recv_buffer = m_recv_buffer.get();
425
0
          recv_buffer = recv_buffer.subspan(m_body_start);
426
0
          m_chunk_pos += chunk_size;
427
0
          if (chunk_size == 0)
428
0
          {
429
0
            TORRENT_ASSERT(m_recv_buffer.get().size() < m_chunk_pos + m_body_start + 1
430
0
              || m_recv_buffer.get()[static_cast<std::ptrdiff_t>(m_chunk_pos + m_body_start)] == 'H'
431
0
              || (m_parser.chunked_encoding()
432
0
                && m_recv_buffer.get()[static_cast<std::ptrdiff_t>(m_chunk_pos + m_body_start)] == '\r'));
433
0
            m_chunk_pos = -1;
434
0
          }
435
0
        }
436
0
      }
437
438
0
      int payload = int(bytes_transferred);
439
0
      if (payload > m_response_left) payload = int(m_response_left);
440
0
      if (payload > front_request.length) payload = front_request.length;
441
      // TODO: technically, this isn't supposed to happen, but it seems to
442
      // sometimes. Some of the accounting is probably wrong in certain
443
      // cases
444
0
      if (payload > outstanding_bytes()) payload = outstanding_bytes();
445
0
      received_bytes(payload, 0);
446
0
      incoming_piece_fragment(payload);
447
0
      m_response_left -= payload;
448
449
0
      if (m_parser.status_code() == 503)
450
0
      {
451
0
        if (!m_parser.finished()) return;
452
453
0
        int retry_time = std::atoi(std::string(recv_buffer.begin(), recv_buffer.end()).c_str());
454
0
        if (retry_time <= 0) retry_time = 60;
455
#ifndef TORRENT_DISABLE_LOGGING
456
        peer_log(peer_log_alert::info, "CONNECT", "retrying in %d seconds", retry_time);
457
#endif
458
459
0
        received_bytes(0, int(bytes_transferred));
460
        // temporarily unavailable, retry later
461
0
        t->retry_web_seed(this, seconds32(retry_time));
462
0
        disconnect(error_code(m_parser.status_code(), http_category()), operation_t::bittorrent, failure);
463
0
        return;
464
0
      }
465
466
467
      // we only received the header, no data
468
0
      if (recv_buffer.empty()) break;
469
470
0
      if (recv_buffer.size() < front_request.length) break;
471
472
      // if the response is chunked, we need to receive the last
473
      // terminating chunk and the tail headers before we can proceed
474
0
      if (m_parser.chunked_encoding() && m_chunk_pos >= 0) break;
475
476
0
      m_requests.pop_front();
477
0
      incoming_piece(front_request, recv_buffer.begin());
478
0
      if (associated_torrent().expired()) return;
479
480
0
      int const size_to_cut = m_body_start + front_request.length;
481
0
      TORRENT_ASSERT(m_recv_buffer.get().size() < size_to_cut + 1
482
0
        || m_recv_buffer.get()[size_to_cut] == 'H'
483
0
        || (m_parser.chunked_encoding() && m_recv_buffer.get()[size_to_cut] == '\r'));
484
485
0
      m_recv_buffer.cut(size_to_cut, t->block_size() + 1024);
486
0
      if (m_response_left == 0) m_chunk_pos = 0;
487
0
      else m_chunk_pos -= front_request.length;
488
0
      TORRENT_ASSERT(bytes_transferred >= aux::numeric_cast<std::size_t>(payload));
489
0
      bytes_transferred -= aux::numeric_cast<std::size_t>(payload);
490
0
      m_body_start = 0;
491
0
      if (m_response_left > 0) continue;
492
0
      TORRENT_ASSERT(m_response_left == 0);
493
0
      m_parser.reset();
494
0
    }
495
0
  }
496
497
  void http_seed_connection::get_specific_peer_info(peer_info& p) const
498
0
  {
499
0
    web_connection_base::get_specific_peer_info(p);
500
0
    p.flags |= peer_info::local_connection;
501
0
    p.connection_type = peer_info::http_seed;
502
0
  }
503
504
}