/src/libtorrent/src/web_peer_connection.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | |
3 | | Copyright (c) 2006-2021, Arvid Norberg |
4 | | Copyright (c) 2016-2017, 2020, Alden Torres |
5 | | Copyright (c) 2016-2017, Andrei Kurushin |
6 | | Copyright (c) 2016, Falcosc |
7 | | Copyright (c) 2016, Steven Siloti |
8 | | Copyright (c) 2017, Pavel Pimenov |
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 "libtorrent/config.hpp" |
40 | | |
41 | | #include <functional> |
42 | | #include <cstdlib> |
43 | | #include <cstdio> // for snprintf |
44 | | #include <cinttypes> // for PRId64 et.al. |
45 | | |
46 | | #include "libtorrent/web_peer_connection.hpp" |
47 | | #include "libtorrent/session.hpp" |
48 | | #include "libtorrent/entry.hpp" |
49 | | #include "libtorrent/bencode.hpp" |
50 | | #include "libtorrent/alert_types.hpp" |
51 | | #include "libtorrent/aux_/invariant_check.hpp" |
52 | | #include "libtorrent/io.hpp" |
53 | | #include "libtorrent/parse_url.hpp" |
54 | | #include "libtorrent/peer_info.hpp" |
55 | | #include "libtorrent/aux_/session_interface.hpp" |
56 | | #include "libtorrent/aux_/alert_manager.hpp" // for alert_manager |
57 | | #include "libtorrent/aux_/escape_string.hpp" // for escape_path |
58 | | #include "libtorrent/hex.hpp" // for is_hex |
59 | | #include "libtorrent/random.hpp" |
60 | | #include "libtorrent/torrent.hpp" |
61 | | #include "libtorrent/http_parser.hpp" |
62 | | |
63 | | namespace libtorrent { |
64 | | |
65 | | constexpr int request_size_overhead = 5000; |
66 | | |
67 | | std::string escape_file_path(file_storage const& storage, file_index_t index); |
68 | | |
69 | | web_peer_connection::web_peer_connection(peer_connection_args& pack |
70 | | , web_seed_t& web) |
71 | 0 | : web_connection_base(pack, web) |
72 | 0 | , m_url(web.url) |
73 | 0 | , m_web(&web) |
74 | 0 | , m_received_body(0) |
75 | 0 | , m_chunk_pos(0) |
76 | 0 | , m_partial_chunk_header(0) |
77 | 0 | , m_num_responses(0) |
78 | 0 | { |
79 | 0 | INVARIANT_CHECK; |
80 | |
|
81 | 0 | if (!m_settings.get_bool(settings_pack::report_web_seed_downloads)) |
82 | 0 | ignore_stats(true); |
83 | |
|
84 | 0 | std::shared_ptr<torrent> tor = pack.tor.lock(); |
85 | 0 | TORRENT_ASSERT(tor); |
86 | | |
87 | | // if the web server is known not to support keep-alive. request 4MiB |
88 | | // but we want to have at least piece size to prevent block based requests |
89 | 0 | int const min_size = std::max((web.supports_keepalive ? 1 : 4) * 1024 * 1024, |
90 | 0 | tor->torrent_file().piece_length()); |
91 | | |
92 | | // we prefer downloading large chunks from web seeds, |
93 | | // but still want to be able to split requests |
94 | 0 | int const preferred_size = std::max(min_size, m_settings.get_int(settings_pack::urlseed_max_request_bytes)); |
95 | |
|
96 | 0 | prefer_contiguous_blocks(preferred_size / tor->block_size()); |
97 | |
|
98 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
99 | 0 | bool const single_file_request = t->torrent_file().num_files() == 1; |
100 | |
|
101 | 0 | if (!single_file_request) |
102 | 0 | { |
103 | | // handle incorrect .torrent files which are multi-file |
104 | | // but have web seeds not ending with a slash |
105 | 0 | ensure_trailing_slash(m_path); |
106 | 0 | ensure_trailing_slash(m_url); |
107 | 0 | } |
108 | 0 | else |
109 | 0 | { |
110 | | // handle .torrent files that don't include the filename in the url |
111 | 0 | if (m_path.empty()) m_path += '/'; |
112 | 0 | if (m_path[m_path.size() - 1] == '/') |
113 | 0 | { |
114 | 0 | m_path += escape_string(t->torrent_file().name()); |
115 | 0 | } |
116 | |
|
117 | 0 | if (!m_url.empty() && m_url[m_url.size() - 1] == '/') |
118 | 0 | { |
119 | 0 | m_url += escape_file_path(t->torrent_file().orig_files(), file_index_t(0)); |
120 | 0 | } |
121 | 0 | } |
122 | | |
123 | | // we want large blocks as well, so |
124 | | // we can request more bytes at once |
125 | | // this setting will merge adjacent requests |
126 | | // into single larger ones |
127 | 0 | request_large_blocks(true); |
128 | |
|
129 | | #ifndef TORRENT_DISABLE_LOGGING |
130 | | peer_log(peer_log_alert::info, "URL", "web_peer_connection %s", m_url.c_str()); |
131 | | #endif |
132 | 0 | } |
133 | | |
134 | | std::string escape_file_path(file_storage const& storage, file_index_t index) |
135 | 0 | { |
136 | 0 | std::string new_path { storage.file_path(index) }; |
137 | | #ifdef TORRENT_WINDOWS |
138 | | convert_path_to_posix(new_path); |
139 | | #endif |
140 | 0 | return escape_path(new_path); |
141 | 0 | } |
142 | | |
143 | | void web_peer_connection::on_connected() |
144 | 0 | { |
145 | 0 | peer_id pid; |
146 | 0 | aux::random_bytes(pid); |
147 | 0 | set_pid(pid); |
148 | |
|
149 | 0 | if (m_web->have_files.empty()) |
150 | 0 | { |
151 | 0 | incoming_have_all(); |
152 | 0 | } |
153 | 0 | else if (m_web->have_files.none_set()) |
154 | 0 | { |
155 | 0 | incoming_have_none(); |
156 | 0 | m_web->interesting = false; |
157 | | #ifndef TORRENT_DISABLE_LOGGING |
158 | | peer_log(peer_log_alert::info, "WEB-SEED", "have no files, not interesting. %s", m_url.c_str()); |
159 | | #endif |
160 | 0 | } |
161 | 0 | else |
162 | 0 | { |
163 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
164 | | |
165 | | // only advertise pieces that are contained within the files we have as |
166 | | // indicated by m_web->have_files AND padfiles! |
167 | | // it's important to include pieces that may overlap many files, as long |
168 | | // as we have all those files, so instead of starting with a clear bitfield |
169 | | // and setting the pieces corresponding to files we have, we do it the |
170 | | // other way around. Start with assuming we have all files, and clear |
171 | | // pieces overlapping with files we *don't* have. |
172 | 0 | typed_bitfield<piece_index_t> have; |
173 | 0 | file_storage const& fs = t->torrent_file().files(); |
174 | 0 | have.resize(fs.num_pieces(), true); |
175 | 0 | for (auto const i : fs.file_range()) |
176 | 0 | { |
177 | | // if we have the file, no need to do anything |
178 | 0 | if (m_web->have_files.get_bit(i) || fs.pad_file_at(i)) continue; |
179 | | |
180 | 0 | auto const range = aux::file_piece_range_inclusive(fs, i); |
181 | 0 | for (piece_index_t k = std::get<0>(range); k < std::get<1>(range); ++k) |
182 | 0 | have.clear_bit(k); |
183 | 0 | } |
184 | 0 | t->set_seed(peer_info_struct(), false); |
185 | 0 | if (have.none_set()) |
186 | 0 | { |
187 | 0 | incoming_have_none(); |
188 | 0 | m_web->interesting = false; |
189 | | #ifndef TORRENT_DISABLE_LOGGING |
190 | | peer_log(peer_log_alert::info, "WEB-SEED", "have no pieces, not interesting. %s", m_url.c_str()); |
191 | | #endif |
192 | 0 | } |
193 | 0 | else |
194 | 0 | { |
195 | 0 | incoming_bitfield(have); |
196 | 0 | } |
197 | 0 | } |
198 | | |
199 | | // TODO: 3 this should be an optional<piece_index_t>, piece index -1 should |
200 | | // not be allowed |
201 | 0 | if (m_web->restart_request.piece != piece_index_t(-1)) |
202 | 0 | { |
203 | | // increase the chances of requesting the block |
204 | | // we have partial data for already, to finish it |
205 | 0 | incoming_suggest(m_web->restart_request.piece); |
206 | 0 | } |
207 | 0 | web_connection_base::on_connected(); |
208 | 0 | } |
209 | | |
210 | | void web_peer_connection::disconnect(error_code const& ec |
211 | | , operation_t op, disconnect_severity_t const error) |
212 | 0 | { |
213 | 0 | if (is_disconnecting()) return; |
214 | | |
215 | 0 | if (op == operation_t::sock_write && ec == boost::system::errc::broken_pipe) |
216 | 0 | { |
217 | | #ifndef TORRENT_DISABLE_LOGGING |
218 | | // a write operation failed with broken-pipe. This typically happens |
219 | | // with HTTP 1.0 servers that close their incoming channel of the TCP |
220 | | // stream whenever they're done reading one full request. Instead of |
221 | | // us bailing out and failing the entire request just because our |
222 | | // write-end was closed, ignore it and keep reading until the read-end |
223 | | // also is closed. |
224 | | peer_log(peer_log_alert::info, "WRITE_DIRECTION", "CLOSED"); |
225 | | #endif |
226 | | |
227 | | // prevent the peer from trying to send anything more |
228 | 0 | m_send_buffer.clear(); |
229 | | |
230 | | // when the web server closed our write-end of the socket (i.e. its |
231 | | // read-end), if it's an HTTP 1.0 server. we will stop sending more |
232 | | // requests. We'll close the connection once we receive the last bytes, |
233 | | // and our read end is closed as well. |
234 | 0 | incoming_choke(); |
235 | 0 | return; |
236 | 0 | } |
237 | | |
238 | 0 | if (op == operation_t::connect && m_web && !m_web->endpoints.empty()) |
239 | 0 | { |
240 | | // we failed to connect to this IP. remove it so that the next attempt |
241 | | // uses the next IP in the list. |
242 | 0 | m_web->endpoints.erase(m_web->endpoints.begin()); |
243 | 0 | } |
244 | |
|
245 | 0 | if (ec == errors::uninteresting_upload_peer && m_web) |
246 | 0 | { |
247 | | // if this is an "ephemeral" web seed, it means it was added by receiving |
248 | | // an HTTP redirect. If we disconnect because we're not interested in any |
249 | | // of its pieces, mark it as uninteresting, to avoid reconnecting to it |
250 | | // repeatedly |
251 | 0 | if (m_web->ephemeral) m_web->interesting = false; |
252 | | |
253 | | // if the web seed is not ephemeral, but we're still not interested. That |
254 | | // implies that all files either have failed with 404 or with a |
255 | | // redirection to a different web server. |
256 | 0 | m_web->retry = std::max(m_web->retry, aux::time_now32() |
257 | 0 | + seconds32(m_settings.get_int(settings_pack::urlseed_wait_retry))); |
258 | 0 | TORRENT_ASSERT(m_web->retry > aux::time_now32()); |
259 | 0 | } |
260 | |
|
261 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
262 | |
|
263 | 0 | if (!m_requests.empty() && !m_file_requests.empty() |
264 | 0 | && !m_piece.empty() && m_web) |
265 | 0 | { |
266 | | #ifndef TORRENT_DISABLE_LOGGING |
267 | | if (should_log(peer_log_alert::info)) |
268 | | { |
269 | | peer_log(peer_log_alert::info, "SAVE_RESTART_DATA" |
270 | | , "data: %d req: %d off: %d" |
271 | | , int(m_piece.size()), int(m_requests.front().piece) |
272 | | , m_requests.front().start); |
273 | | } |
274 | | #endif |
275 | 0 | m_web->restart_request = m_requests.front(); |
276 | 0 | if (!m_web->restart_piece.empty()) |
277 | 0 | { |
278 | | // we're about to replace a different restart piece |
279 | | // buffer. So it was wasted download |
280 | 0 | if (t) t->add_redundant_bytes(int(m_web->restart_piece.size()) |
281 | 0 | , waste_reason::piece_closing); |
282 | 0 | } |
283 | 0 | m_web->restart_piece = std::move(m_piece); |
284 | | |
285 | | // we have to do this to not count this data as redundant. The |
286 | | // upper layer will call downloading_piece_progress and assume |
287 | | // it's all wasted download. Since we're saving it here, it isn't. |
288 | 0 | m_requests.clear(); |
289 | 0 | } |
290 | |
|
291 | 0 | if (m_web && !m_web->supports_keepalive && error == peer_connection_interface::normal) |
292 | 0 | { |
293 | | // if the web server doesn't support keepalive and we were |
294 | | // disconnected as a graceful EOF, reconnect right away |
295 | 0 | if (t) post(get_context() |
296 | 0 | , std::bind(&torrent::maybe_connect_web_seeds, t)); |
297 | 0 | } |
298 | |
|
299 | 0 | if (error >= failure) |
300 | 0 | { |
301 | 0 | m_web->retry = std::max(m_web->retry, aux::time_now32() |
302 | 0 | + seconds32(m_settings.get_int(settings_pack::urlseed_wait_retry))); |
303 | 0 | } |
304 | |
|
305 | 0 | peer_connection::disconnect(ec, op, error); |
306 | 0 | TORRENT_ASSERT(m_web->resolving == false); |
307 | 0 | m_web->peer_info.connection = nullptr; |
308 | 0 | } |
309 | | |
310 | | piece_block_progress web_peer_connection::downloading_piece_progress() const |
311 | 0 | { |
312 | 0 | if (m_requests.empty()) return {}; |
313 | | |
314 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
315 | 0 | TORRENT_ASSERT(t); |
316 | |
|
317 | 0 | piece_block_progress ret; |
318 | |
|
319 | 0 | ret.piece_index = m_requests.front().piece; |
320 | 0 | ret.bytes_downloaded = int(m_piece.size()); |
321 | | // this is used to make sure that the block_index stays within |
322 | | // bounds. If the entire piece is downloaded, the block_index |
323 | | // would otherwise point to one past the end |
324 | 0 | int correction = m_piece.empty() ? 0 : -1; |
325 | 0 | ret.block_index = (m_requests.front().start + int(m_piece.size()) + correction) / t->block_size(); |
326 | 0 | TORRENT_ASSERT(ret.block_index < int(piece_block::invalid.block_index)); |
327 | 0 | TORRENT_ASSERT(ret.piece_index < piece_block::invalid.piece_index); |
328 | |
|
329 | 0 | ret.full_block_bytes = t->block_size(); |
330 | 0 | piece_index_t const last_piece = t->torrent_file().last_piece(); |
331 | 0 | if (ret.piece_index == last_piece && ret.block_index |
332 | 0 | == t->torrent_file().piece_size(last_piece) / t->block_size()) |
333 | 0 | { |
334 | 0 | ret.full_block_bytes = t->torrent_file().piece_size(last_piece) % t->block_size(); |
335 | 0 | } |
336 | 0 | return ret; |
337 | 0 | } |
338 | | |
339 | | void web_peer_connection::write_request(peer_request const& r) |
340 | 0 | { |
341 | 0 | INVARIANT_CHECK; |
342 | |
|
343 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
344 | 0 | TORRENT_ASSERT(t); |
345 | |
|
346 | 0 | TORRENT_ASSERT(t->valid_metadata()); |
347 | |
|
348 | 0 | torrent_info const& info = t->torrent_file(); |
349 | 0 | peer_request req = r; |
350 | |
|
351 | 0 | std::string request; |
352 | 0 | request.reserve(400); |
353 | |
|
354 | 0 | int size = r.length; |
355 | 0 | const int block_size = t->block_size(); |
356 | 0 | const int piece_size = t->torrent_file().piece_length(); |
357 | 0 | peer_request pr{}; |
358 | |
|
359 | 0 | while (size > 0) |
360 | 0 | { |
361 | 0 | int request_offset = r.start + r.length - size; |
362 | 0 | pr.start = request_offset % piece_size; |
363 | 0 | pr.length = std::min(std::min(block_size, size), piece_size - pr.start); |
364 | 0 | pr.piece = piece_index_t(static_cast<int>(r.piece) + request_offset / piece_size); |
365 | 0 | TORRENT_ASSERT(validate_piece_request(pr)); |
366 | 0 | m_requests.push_back(pr); |
367 | |
|
368 | 0 | if (m_web->restart_request == m_requests.front()) |
369 | 0 | { |
370 | 0 | TORRENT_ASSERT(int(m_piece.size()) == m_received_in_piece); |
371 | 0 | TORRENT_ASSERT(m_piece.empty()); |
372 | 0 | m_piece = std::move(m_web->restart_piece); |
373 | 0 | peer_request const& front = m_requests.front(); |
374 | 0 | TORRENT_ASSERT(front.length > int(m_piece.size())); |
375 | |
|
376 | | #ifndef TORRENT_DISABLE_LOGGING |
377 | | peer_log(peer_log_alert::info, "RESTART_DATA", |
378 | | "data: %d req: (%d, %d) size: %d" |
379 | | , int(m_piece.size()), static_cast<int>(front.piece), front.start |
380 | | , front.start + front.length - 1); |
381 | | #else |
382 | 0 | TORRENT_UNUSED(front); |
383 | 0 | #endif |
384 | |
|
385 | 0 | req.start += int(m_piece.size()); |
386 | 0 | req.length -= int(m_piece.size()); |
387 | | |
388 | | // just to keep the accounting straight for the upper layer. |
389 | | // it doesn't know we just re-wrote the request |
390 | 0 | incoming_piece_fragment(int(m_piece.size())); |
391 | 0 | m_web->restart_request.piece = piece_index_t(-1); |
392 | |
|
393 | 0 | TORRENT_ASSERT(int(m_piece.size()) == m_received_in_piece); |
394 | 0 | } |
395 | |
|
396 | | #if 0 |
397 | | std::cerr << this << " REQ: p: " << pr.piece << " " << pr.start << std::endl; |
398 | | #endif |
399 | 0 | size -= pr.length; |
400 | 0 | } |
401 | |
|
402 | | #ifndef TORRENT_DISABLE_LOGGING |
403 | | peer_log(peer_log_alert::outgoing_message, "REQUESTING", "(piece: %d start: %d) - (piece: %d end: %d)" |
404 | | , static_cast<int>(r.piece), r.start |
405 | | , static_cast<int>(pr.piece), pr.start + pr.length); |
406 | | #endif |
407 | |
|
408 | 0 | bool const single_file_request = t->torrent_file().num_files() == 1; |
409 | 0 | int const proxy_type = m_settings.get_int(settings_pack::proxy_type); |
410 | 0 | bool const using_proxy = (proxy_type == settings_pack::http |
411 | 0 | || proxy_type == settings_pack::http_pw) && !m_ssl; |
412 | | |
413 | | // the number of pad files that have been "requested". In case we _only_ |
414 | | // request padfiles, we can't rely on handling them in the on_receive() |
415 | | // callback (because we won't receive anything), instead we have to post a |
416 | | // pretend read callback where we can deliver the zeroes for the partfile |
417 | 0 | int num_pad_files = 0; |
418 | | |
419 | | // TODO: 3 do we really need a special case here? wouldn't the multi-file |
420 | | // case handle single file torrents correctly too? |
421 | 0 | if (single_file_request) |
422 | 0 | { |
423 | 0 | file_request_t file_req; |
424 | 0 | file_req.file_index = file_index_t(0); |
425 | 0 | file_req.start = std::int64_t(static_cast<int>(req.piece)) * info.piece_length() |
426 | 0 | + req.start; |
427 | 0 | file_req.length = req.length; |
428 | |
|
429 | 0 | request += "GET "; |
430 | | // do not encode single file paths, they are |
431 | | // assumed to be encoded in the torrent file |
432 | 0 | request += using_proxy ? m_url : m_path; |
433 | 0 | request += " HTTP/1.1\r\n"; |
434 | 0 | add_headers(request, m_settings, using_proxy); |
435 | 0 | request += "\r\nRange: bytes="; |
436 | 0 | request += to_string(file_req.start).data(); |
437 | 0 | request += "-"; |
438 | 0 | request += to_string(file_req.start + file_req.length - 1).data(); |
439 | 0 | request += "\r\n\r\n"; |
440 | 0 | m_first_request = false; |
441 | |
|
442 | 0 | m_file_requests.push_back(file_req); |
443 | 0 | } |
444 | 0 | else |
445 | 0 | { |
446 | 0 | std::vector<file_slice> files = info.orig_files().map_block(req.piece, req.start |
447 | 0 | , req.length); |
448 | |
|
449 | 0 | for (auto const &f : files) |
450 | 0 | { |
451 | 0 | file_request_t file_req; |
452 | 0 | file_req.file_index = f.file_index; |
453 | 0 | file_req.start = f.offset; |
454 | 0 | file_req.length = int(f.size); |
455 | |
|
456 | 0 | if (info.orig_files().pad_file_at(f.file_index)) |
457 | 0 | { |
458 | 0 | m_file_requests.push_back(file_req); |
459 | 0 | ++num_pad_files; |
460 | 0 | continue; |
461 | 0 | } |
462 | | |
463 | 0 | request += "GET "; |
464 | 0 | if (using_proxy) |
465 | 0 | { |
466 | | // m_url is already a properly escaped URL |
467 | | // with the correct slashes. Don't encode it again |
468 | 0 | request += m_url; |
469 | 0 | } |
470 | |
|
471 | 0 | auto redirection = m_web->redirects.find(f.file_index); |
472 | 0 | if (redirection != m_web->redirects.end()) |
473 | 0 | { |
474 | 0 | auto const& redirect = redirection->second; |
475 | | // in case of http proxy "request" already contains m_url with trailing slash, so let's skip dup slash |
476 | 0 | bool const trailing_slash = using_proxy && !redirect.empty() && redirect[0] == '/'; |
477 | 0 | request.append(redirect, trailing_slash, std::string::npos); |
478 | 0 | } |
479 | 0 | else |
480 | 0 | { |
481 | 0 | if (!using_proxy) |
482 | 0 | { |
483 | | // m_path is already a properly escaped URL |
484 | | // with the correct slashes. Don't encode it again |
485 | 0 | request += m_path; |
486 | 0 | } |
487 | |
|
488 | 0 | request += escape_file_path(info.orig_files(), f.file_index); |
489 | 0 | } |
490 | 0 | request += " HTTP/1.1\r\n"; |
491 | 0 | add_headers(request, m_settings, using_proxy); |
492 | 0 | request += "\r\nRange: bytes="; |
493 | 0 | request += to_string(f.offset).data(); |
494 | 0 | request += "-"; |
495 | 0 | request += to_string(f.offset + f.size - 1).data(); |
496 | 0 | request += "\r\n\r\n"; |
497 | 0 | m_first_request = false; |
498 | |
|
499 | | #if 0 |
500 | | std::cerr << this << " SEND-REQUEST: f: " << f.file_index |
501 | | << " s: " << f.offset |
502 | | << " e: " << (f.offset + f.size - 1) << std::endl; |
503 | | #endif |
504 | | // TODO: 3 file_index_t should not allow negative values |
505 | 0 | TORRENT_ASSERT(f.file_index >= file_index_t(0)); |
506 | |
|
507 | 0 | m_file_requests.push_back(file_req); |
508 | 0 | } |
509 | 0 | } |
510 | |
|
511 | 0 | if (num_pad_files == int(m_file_requests.size())) |
512 | 0 | { |
513 | 0 | post(get_context(), std::bind( |
514 | 0 | &web_peer_connection::on_receive_padfile, |
515 | 0 | std::static_pointer_cast<web_peer_connection>(self()))); |
516 | 0 | return; |
517 | 0 | } |
518 | | |
519 | | #ifndef TORRENT_DISABLE_LOGGING |
520 | | peer_log(peer_log_alert::outgoing_message, "REQUEST", "%s", request.c_str()); |
521 | | #endif |
522 | | |
523 | 0 | send_buffer(request); |
524 | 0 | } |
525 | | |
526 | | namespace { |
527 | | |
528 | | std::string get_peer_name(http_parser const& p, std::string const& host) |
529 | 0 | { |
530 | 0 | std::string const& server_version = p.header("server"); |
531 | 0 | if (!server_version.empty()) |
532 | 0 | return server_version; |
533 | 0 | return host; |
534 | 0 | } |
535 | | |
536 | | std::tuple<std::int64_t, std::int64_t> get_range( |
537 | | http_parser const& parser, error_code& ec) |
538 | 0 | { |
539 | 0 | std::int64_t range_start; |
540 | 0 | std::int64_t range_end; |
541 | 0 | if (parser.status_code() == 206) |
542 | 0 | { |
543 | 0 | std::tie(range_start, range_end) = parser.content_range(); |
544 | 0 | if (range_start < 0 || range_end < range_start) |
545 | 0 | { |
546 | 0 | ec = errors::invalid_range; |
547 | 0 | range_start = 0; |
548 | 0 | range_end = 0; |
549 | 0 | } |
550 | 0 | else |
551 | 0 | { |
552 | | // the http range is inclusive |
553 | 0 | range_end++; |
554 | 0 | } |
555 | 0 | } |
556 | 0 | else |
557 | 0 | { |
558 | 0 | range_start = 0; |
559 | 0 | range_end = parser.content_length(); |
560 | 0 | if (range_end < 0) |
561 | 0 | { |
562 | 0 | range_end = 0; |
563 | 0 | ec = errors::no_content_length; |
564 | 0 | } |
565 | 0 | } |
566 | 0 | return std::make_tuple(range_start, range_end); |
567 | 0 | } |
568 | | } |
569 | | |
570 | | // -------------------------- |
571 | | // RECEIVE DATA |
572 | | // -------------------------- |
573 | | |
574 | | bool web_peer_connection::received_invalid_data(piece_index_t const index, bool single_peer) |
575 | 0 | { |
576 | 0 | if (!single_peer) return peer_connection::received_invalid_data(index, single_peer); |
577 | | |
578 | | // when a web seed fails a hash check, do the following: |
579 | | // 1. if the whole piece only overlaps a single file, mark that file as not |
580 | | // have for this peer |
581 | | // 2. if the piece overlaps more than one file, mark the piece as not have |
582 | | // for this peer |
583 | | // 3. if it's a single file torrent, just ban it right away |
584 | | // this handles the case where web seeds may have some files updated but not other |
585 | | |
586 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
587 | 0 | file_storage const& fs = t->torrent_file().files(); |
588 | | |
589 | | // single file torrent |
590 | 0 | if (fs.num_files() == 1) return peer_connection::received_invalid_data(index, single_peer); |
591 | | |
592 | 0 | std::vector<file_slice> files = fs.map_block(index, 0, fs.piece_size(index)); |
593 | |
|
594 | 0 | if (files.size() == 1) |
595 | 0 | { |
596 | | // assume the web seed has a different copy of this specific file |
597 | | // than what we expect, and pretend not to have it. |
598 | 0 | auto const range = file_piece_range_inclusive(fs, files[0].file_index); |
599 | 0 | for (piece_index_t i = std::get<0>(range); i != std::get<1>(range); ++i) |
600 | 0 | incoming_dont_have(i); |
601 | 0 | } |
602 | 0 | else |
603 | 0 | { |
604 | 0 | incoming_dont_have(index); |
605 | 0 | } |
606 | |
|
607 | 0 | peer_connection::received_invalid_data(index, single_peer); |
608 | | |
609 | | // if we don't think we have any of the files, allow banning the web seed |
610 | 0 | if (num_have_pieces() == 0) return true; |
611 | | |
612 | | // don't disconnect, we won't request anything from this file again |
613 | 0 | return false; |
614 | 0 | } |
615 | | |
616 | | void web_peer_connection::on_receive_padfile() |
617 | 0 | { |
618 | 0 | handle_padfile(); |
619 | 0 | } |
620 | | |
621 | | void web_peer_connection::handle_error(int const bytes_left) |
622 | 0 | { |
623 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
624 | 0 | TORRENT_ASSERT(t); |
625 | | |
626 | | // TODO: 2 just make this peer not have the pieces |
627 | | // associated with the file we just requested. Only |
628 | | // when it doesn't have any of the file do the following |
629 | | // pad files will make it complicated |
630 | | |
631 | | // temporarily unavailable, retry later |
632 | 0 | t->retry_web_seed(this, m_parser.header_duration("retry-after")); |
633 | 0 | if (t->alerts().should_post<url_seed_alert>()) |
634 | 0 | { |
635 | 0 | std::string const error_msg = to_string(m_parser.status_code()).data() |
636 | 0 | + (" " + m_parser.message()); |
637 | 0 | t->alerts().emplace_alert<url_seed_alert>(t->get_handle(), m_url |
638 | 0 | , error_msg); |
639 | 0 | } |
640 | 0 | received_bytes(0, bytes_left); |
641 | 0 | disconnect(error_code(m_parser.status_code(), http_category()), operation_t::bittorrent, failure); |
642 | 0 | } |
643 | | |
644 | | void web_peer_connection::disable(error_code const& ec) |
645 | 0 | { |
646 | | // we should not try this server again. |
647 | 0 | m_web->disabled = true; |
648 | 0 | disconnect(ec, operation_t::bittorrent, peer_error); |
649 | 0 | if (m_web->ephemeral) |
650 | 0 | { |
651 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
652 | 0 | TORRENT_ASSERT(t); |
653 | 0 | t->remove_web_seed_conn(this); |
654 | 0 | } |
655 | 0 | m_web = nullptr; |
656 | 0 | TORRENT_ASSERT(is_disconnecting()); |
657 | 0 | } |
658 | | |
659 | | void web_peer_connection::handle_redirect(int const bytes_left) |
660 | 0 | { |
661 | | // this means we got a redirection request |
662 | | // look for the location header |
663 | 0 | std::string location = m_parser.header("location"); |
664 | 0 | received_bytes(0, bytes_left); |
665 | |
|
666 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
667 | 0 | TORRENT_ASSERT(t); |
668 | |
|
669 | 0 | if (location.empty()) |
670 | 0 | { |
671 | 0 | disable(errors::missing_location); |
672 | 0 | return; |
673 | 0 | } |
674 | | |
675 | 0 | bool const single_file_request = !m_path.empty() |
676 | 0 | && m_path[m_path.size() - 1] != '/'; |
677 | | |
678 | | // when SSRF mitigation is enabled, a web seed on the internet (is_global()) |
679 | | // is not allowed to redirect to a server on the local network, so we set |
680 | | // the no_local_ips flag |
681 | 0 | auto const web_seed_flags = torrent::ephemeral |
682 | 0 | | ((m_settings.get_bool(settings_pack::ssrf_mitigation) && aux::is_global(remote().address())) |
683 | 0 | ? torrent::no_local_ips : web_seed_flag_t{}); |
684 | | |
685 | | // add the redirected url and remove the current one |
686 | 0 | if (!single_file_request) |
687 | 0 | { |
688 | 0 | TORRENT_ASSERT(!m_file_requests.empty()); |
689 | 0 | file_index_t const file_index = m_file_requests.front().file_index; |
690 | |
|
691 | 0 | location = resolve_redirect_location(m_url, location); |
692 | | #ifndef TORRENT_DISABLE_LOGGING |
693 | | peer_log(peer_log_alert::info, "LOCATION", "%s", location.c_str()); |
694 | | #endif |
695 | | // TODO: 3 this could be made more efficient for the case when we use an |
696 | | // HTTP proxy. Then we wouldn't need to add new web seeds to the torrent, |
697 | | // we could just make the redirect table contain full URLs. |
698 | 0 | std::string redirect_base; |
699 | 0 | std::string redirect_path; |
700 | 0 | error_code ec; |
701 | 0 | std::tie(redirect_base, redirect_path) = split_url(location, ec); |
702 | |
|
703 | 0 | if (ec) |
704 | 0 | { |
705 | | // we should not try this server again. |
706 | 0 | disconnect(errors::missing_location, operation_t::bittorrent, failure); |
707 | 0 | return; |
708 | 0 | } |
709 | | |
710 | | // add_web_seed won't add duplicates. If we have already added an entry |
711 | | // with this URL, we'll get back the existing entry |
712 | | |
713 | | // "ephemeral" flag should be set to avoid "web_seed_t" saving in resume data. |
714 | | // E.g. original "web_seed_t" request url points to "http://example1.com/file1" and |
715 | | // web server responses with redirect location "http://example2.com/subpath/file2". |
716 | | // "handle_redirect" process this location to create new "web_seed_t" |
717 | | // with base url=="http://example2.com/" and redirects[0]=="/subpath/file2"). |
718 | | // If we try to load resume with such "web_seed_t" then "web_peer_connection" will send |
719 | | // request with wrong path "http://example2.com/file1" (cause "redirects" map is not serialized in resume) |
720 | 0 | web_seed_t* web = t->add_web_seed(redirect_base, web_seed_entry::url_seed |
721 | 0 | , m_external_auth, m_extra_headers, web_seed_flags); |
722 | 0 | web->have_files.resize(t->torrent_file().num_files(), false); |
723 | | |
724 | | // the new web seed we're adding only has this file for now |
725 | | // we may add more files later |
726 | 0 | web->redirects[file_index] = redirect_path; |
727 | 0 | if (web->have_files.get_bit(file_index) == false) |
728 | 0 | { |
729 | 0 | web->have_files.set_bit(file_index); |
730 | |
|
731 | 0 | if (web->peer_info.connection != nullptr) |
732 | 0 | { |
733 | 0 | auto* pc = static_cast<peer_connection*>(web->peer_info.connection); |
734 | | |
735 | | // we just learned that this host has this file, and we're currently |
736 | | // connected to it. Make it advertise that it has this file to the |
737 | | // bittorrent engine |
738 | 0 | file_storage const& fs = t->torrent_file().files(); |
739 | 0 | auto const range = aux::file_piece_range_inclusive(fs, file_index); |
740 | 0 | for (piece_index_t i = std::get<0>(range); i < std::get<1>(range); ++i) |
741 | 0 | pc->incoming_have(i); |
742 | 0 | } |
743 | | // we just learned about another file this web server has, make sure |
744 | | // it's marked interesting to enable connecting to it |
745 | 0 | web->interesting = true; |
746 | 0 | } |
747 | | |
748 | | // we don't have this file on this server. Don't ask for it again |
749 | 0 | m_web->have_files.resize(t->torrent_file().num_files(), true); |
750 | 0 | if (m_web->have_files[file_index]) |
751 | 0 | { |
752 | 0 | m_web->have_files.clear_bit(file_index); |
753 | | #ifndef TORRENT_DISABLE_LOGGING |
754 | | peer_log(peer_log_alert::info, "MISSING_FILE", "redirection | file: %d" |
755 | | , static_cast<int>(file_index)); |
756 | | #endif |
757 | 0 | } |
758 | 0 | disconnect(errors::redirecting, operation_t::bittorrent, normal); |
759 | 0 | } |
760 | 0 | else |
761 | 0 | { |
762 | 0 | location = resolve_redirect_location(m_url, location); |
763 | | #ifndef TORRENT_DISABLE_LOGGING |
764 | | peer_log(peer_log_alert::info, "LOCATION", "%s", location.c_str()); |
765 | | #endif |
766 | 0 | t->add_web_seed(location, web_seed_entry::url_seed, m_external_auth |
767 | 0 | , m_extra_headers, web_seed_flags); |
768 | | |
769 | | // this web seed doesn't have any files. Don't try to request from it |
770 | | // again this session |
771 | 0 | m_web->have_files.resize(t->torrent_file().num_files(), false); |
772 | 0 | disconnect(errors::redirecting, operation_t::bittorrent, normal); |
773 | 0 | m_web = nullptr; |
774 | 0 | TORRENT_ASSERT(is_disconnecting()); |
775 | 0 | } |
776 | 0 | } |
777 | | |
778 | | void web_peer_connection::on_receive(error_code const& error |
779 | | , std::size_t bytes_transferred) |
780 | 0 | { |
781 | 0 | INVARIANT_CHECK; |
782 | |
|
783 | 0 | if (error) |
784 | 0 | { |
785 | 0 | received_bytes(0, int(bytes_transferred)); |
786 | | #ifndef TORRENT_DISABLE_LOGGING |
787 | | if (should_log(peer_log_alert::info)) |
788 | | { |
789 | | peer_log(peer_log_alert::info, "ERROR" |
790 | | , "web_peer_connection error: %s", error.message().c_str()); |
791 | | } |
792 | | #endif |
793 | 0 | return; |
794 | 0 | } |
795 | | |
796 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
797 | 0 | TORRENT_ASSERT(t); |
798 | | |
799 | | // in case the first file on this series of requests is a padfile |
800 | | // we need to handle it right now |
801 | 0 | span<char const> recv_buffer = m_recv_buffer.get(); |
802 | 0 | handle_padfile(); |
803 | 0 | if (associated_torrent().expired()) return; |
804 | | |
805 | 0 | for (;;) |
806 | 0 | { |
807 | 0 | int payload; |
808 | 0 | int protocol; |
809 | 0 | bool header_finished = m_parser.header_finished(); |
810 | 0 | if (!header_finished) |
811 | 0 | { |
812 | 0 | bool failed = false; |
813 | 0 | std::tie(payload, protocol) = m_parser.incoming(recv_buffer, failed); |
814 | 0 | received_bytes(0, protocol); |
815 | 0 | TORRENT_ASSERT(int(recv_buffer.size()) >= protocol); |
816 | |
|
817 | 0 | if (failed) |
818 | 0 | { |
819 | 0 | received_bytes(0, int(recv_buffer.size())); |
820 | | #ifndef TORRENT_DISABLE_LOGGING |
821 | | if (should_log(peer_log_alert::info)) |
822 | | { |
823 | | peer_log(peer_log_alert::info, "RECEIVE_BYTES" |
824 | | , "%*s", int(recv_buffer.size()), recv_buffer.data()); |
825 | | } |
826 | | #endif |
827 | 0 | disconnect(errors::http_parse_error, operation_t::bittorrent, peer_error); |
828 | 0 | return; |
829 | 0 | } |
830 | | |
831 | 0 | TORRENT_ASSERT(recv_buffer.empty() || recv_buffer[0] == 'H'); |
832 | 0 | TORRENT_ASSERT(int(recv_buffer.size()) <= m_recv_buffer.packet_size()); |
833 | | |
834 | | // this means the entire status line hasn't been received yet |
835 | 0 | if (m_parser.status_code() == -1) |
836 | 0 | { |
837 | 0 | TORRENT_ASSERT(payload == 0); |
838 | 0 | break; |
839 | 0 | } |
840 | | |
841 | 0 | if (!m_parser.header_finished()) |
842 | 0 | { |
843 | 0 | TORRENT_ASSERT(payload == 0); |
844 | 0 | break; |
845 | 0 | } |
846 | | |
847 | 0 | m_body_start = m_parser.body_start(); |
848 | 0 | m_received_body = 0; |
849 | 0 | } |
850 | | |
851 | | // we just completed reading the header |
852 | 0 | if (!header_finished) |
853 | 0 | { |
854 | 0 | ++m_num_responses; |
855 | |
|
856 | 0 | if (m_parser.connection_close()) |
857 | 0 | { |
858 | 0 | incoming_choke(); |
859 | 0 | if (m_num_responses == 1) |
860 | 0 | m_web->supports_keepalive = false; |
861 | 0 | } |
862 | |
|
863 | | #ifndef TORRENT_DISABLE_LOGGING |
864 | | if (should_log(peer_log_alert::info)) |
865 | | { |
866 | | peer_log(peer_log_alert::info, "STATUS" |
867 | | , "%d %s", m_parser.status_code(), m_parser.message().c_str()); |
868 | | auto const& headers = m_parser.headers(); |
869 | | for (auto const &i : headers) |
870 | | peer_log(peer_log_alert::info, "STATUS", " %s: %s", i.first.c_str(), i.second.c_str()); |
871 | | } |
872 | | #endif |
873 | | |
874 | | // if the status code is not one of the accepted ones, abort |
875 | 0 | if (!is_ok_status(m_parser.status_code())) |
876 | 0 | { |
877 | 0 | if (!m_file_requests.empty()) |
878 | 0 | { |
879 | 0 | file_request_t const& file_req = m_file_requests.front(); |
880 | 0 | m_web->have_files.resize(t->torrent_file().num_files(), true); |
881 | 0 | m_web->have_files.clear_bit(file_req.file_index); |
882 | |
|
883 | | #ifndef TORRENT_DISABLE_LOGGING |
884 | | peer_log(peer_log_alert::info, "MISSING_FILE", "http-code: %d | file: %d" |
885 | | , m_parser.status_code(), static_cast<int>(file_req.file_index)); |
886 | | #endif |
887 | 0 | } |
888 | 0 | handle_error(int(recv_buffer.size())); |
889 | 0 | return; |
890 | 0 | } |
891 | | |
892 | 0 | if (is_redirect(m_parser.status_code())) |
893 | 0 | { |
894 | 0 | handle_redirect(int(recv_buffer.size())); |
895 | 0 | return; |
896 | 0 | } |
897 | | |
898 | 0 | m_server_string = get_peer_name(m_parser, m_host); |
899 | |
|
900 | 0 | recv_buffer = recv_buffer.subspan(m_body_start); |
901 | |
|
902 | 0 | m_body_start = m_parser.body_start(); |
903 | 0 | m_received_body = 0; |
904 | 0 | } |
905 | | |
906 | | // we only received the header, no data |
907 | 0 | if (recv_buffer.empty()) break; |
908 | | |
909 | | // =================================== |
910 | | // ======= RESPONSE BYTE RANGE ======= |
911 | | // =================================== |
912 | | |
913 | | // despite the HTTP range being inclusive, range_start and range_end are |
914 | | // exclusive to fit better into C++. i.e. range_end points one byte past |
915 | | // the end of the payload |
916 | 0 | std::int64_t range_start; |
917 | 0 | std::int64_t range_end; |
918 | 0 | error_code ec; |
919 | 0 | std::tie(range_start, range_end) = get_range(m_parser, ec); |
920 | 0 | if (ec) |
921 | 0 | { |
922 | 0 | received_bytes(0, int(recv_buffer.size())); |
923 | 0 | disable(ec); |
924 | 0 | return; |
925 | 0 | } |
926 | | |
927 | 0 | TORRENT_ASSERT(!m_file_requests.empty()); |
928 | 0 | file_request_t const& file_req = m_file_requests.front(); |
929 | 0 | if (range_start != file_req.start |
930 | 0 | || range_end != file_req.start + file_req.length) |
931 | 0 | { |
932 | | // the byte range in the http response is different what we expected |
933 | 0 | received_bytes(0, int(recv_buffer.size())); |
934 | |
|
935 | | #ifndef TORRENT_DISABLE_LOGGING |
936 | | if (should_log(peer_log_alert::incoming)) |
937 | | { |
938 | | peer_log(peer_log_alert::incoming, "INVALID HTTP RESPONSE" |
939 | | , "in=(%d, %" PRId64 "-%" PRId64 ") expected=(%d, %" PRId64 "-%" PRId64 ") ]" |
940 | | , static_cast<int>(file_req.file_index), range_start, range_end |
941 | | , static_cast<int>(file_req.file_index), file_req.start, file_req.start + file_req.length - 1); |
942 | | } |
943 | | #endif |
944 | 0 | disconnect(errors::invalid_range, operation_t::bittorrent, peer_error); |
945 | 0 | return; |
946 | 0 | } |
947 | | |
948 | 0 | if (m_parser.chunked_encoding()) |
949 | 0 | { |
950 | | |
951 | | // ========================= |
952 | | // === CHUNKED ENCODING === |
953 | | // ========================= |
954 | |
|
955 | 0 | while (m_chunk_pos >= 0 && !recv_buffer.empty()) |
956 | 0 | { |
957 | | // first deliver any payload we have in the buffer so far, ahead of |
958 | | // the next chunk header. |
959 | 0 | if (m_chunk_pos > 0) |
960 | 0 | { |
961 | 0 | int const copy_size = std::min(m_chunk_pos, int(recv_buffer.size())); |
962 | 0 | TORRENT_ASSERT(copy_size > 0); |
963 | |
|
964 | 0 | if (m_received_body + copy_size > file_req.length) |
965 | 0 | { |
966 | | // the byte range in the http response is different what we expected |
967 | 0 | received_bytes(0, int(recv_buffer.size())); |
968 | |
|
969 | | #ifndef TORRENT_DISABLE_LOGGING |
970 | | peer_log(peer_log_alert::incoming, "INVALID HTTP RESPONSE" |
971 | | , "received body: %d request size: %d" |
972 | | , m_received_body, file_req.length); |
973 | | #endif |
974 | 0 | disconnect(errors::invalid_range, operation_t::bittorrent, peer_error); |
975 | 0 | return; |
976 | 0 | } |
977 | 0 | incoming_payload(recv_buffer.data(), copy_size); |
978 | |
|
979 | 0 | recv_buffer = recv_buffer.subspan(copy_size); |
980 | 0 | m_chunk_pos -= copy_size; |
981 | |
|
982 | 0 | if (recv_buffer.empty()) goto done; |
983 | 0 | } |
984 | | |
985 | 0 | TORRENT_ASSERT(m_chunk_pos == 0); |
986 | |
|
987 | 0 | int header_size = 0; |
988 | 0 | std::int64_t chunk_size = 0; |
989 | 0 | span<char const> chunk_start = recv_buffer.subspan(m_chunk_pos); |
990 | 0 | TORRENT_ASSERT(chunk_start[0] == '\r' |
991 | 0 | || aux::is_hex({chunk_start.data(), 1})); |
992 | 0 | bool const ret = m_parser.parse_chunk_header(chunk_start, &chunk_size, &header_size); |
993 | 0 | if (!ret) |
994 | 0 | { |
995 | 0 | received_bytes(0, int(chunk_start.size()) - m_partial_chunk_header); |
996 | 0 | m_partial_chunk_header = int(chunk_start.size()); |
997 | 0 | goto done; |
998 | 0 | } |
999 | | #ifndef TORRENT_DISABLE_LOGGING |
1000 | | peer_log(peer_log_alert::info, "CHUNKED_ENCODING" |
1001 | | , "parsed chunk: %" PRId64 " header_size: %d" |
1002 | | , chunk_size, header_size); |
1003 | | #endif |
1004 | 0 | received_bytes(0, header_size - m_partial_chunk_header); |
1005 | 0 | m_partial_chunk_header = 0; |
1006 | 0 | TORRENT_ASSERT(chunk_size != 0 |
1007 | 0 | || int(chunk_start.size()) <= header_size || chunk_start[header_size] == 'H'); |
1008 | 0 | TORRENT_ASSERT(m_body_start + m_chunk_pos < INT_MAX); |
1009 | 0 | m_chunk_pos += int(chunk_size); |
1010 | 0 | recv_buffer = recv_buffer.subspan(header_size); |
1011 | | |
1012 | | // a chunk size of zero means the request is complete. Make sure the |
1013 | | // number of payload bytes we've received matches the number we |
1014 | | // requested. If that's not the case, we got an invalid response. |
1015 | 0 | if (chunk_size == 0) |
1016 | 0 | { |
1017 | 0 | TORRENT_ASSERT_VAL(m_chunk_pos == 0, m_chunk_pos); |
1018 | |
|
1019 | 0 | #if TORRENT_USE_ASSERTS |
1020 | 0 | span<char const> chunk = recv_buffer.subspan(m_chunk_pos); |
1021 | 0 | TORRENT_ASSERT(chunk.size() == 0 || chunk[0] == 'H'); |
1022 | 0 | #endif |
1023 | 0 | m_chunk_pos = -1; |
1024 | |
|
1025 | 0 | TORRENT_ASSERT(m_received_body <= file_req.length); |
1026 | 0 | if (m_received_body != file_req.length) |
1027 | 0 | { |
1028 | | // the byte range in the http response is different what we expected |
1029 | 0 | received_bytes(0, int(recv_buffer.size())); |
1030 | |
|
1031 | | #ifndef TORRENT_DISABLE_LOGGING |
1032 | | peer_log(peer_log_alert::incoming, "INVALID HTTP RESPONSE" |
1033 | | , "received body: %d request size: %d" |
1034 | | , m_received_body, file_req.length); |
1035 | | #endif |
1036 | 0 | disconnect(errors::invalid_range, operation_t::bittorrent, peer_error); |
1037 | 0 | return; |
1038 | 0 | } |
1039 | | // we just completed an HTTP file request. pop it from m_file_requests |
1040 | 0 | m_file_requests.pop_front(); |
1041 | 0 | m_parser.reset(); |
1042 | 0 | m_body_start = 0; |
1043 | 0 | m_received_body = 0; |
1044 | 0 | m_chunk_pos = 0; |
1045 | 0 | m_partial_chunk_header = 0; |
1046 | | |
1047 | | // in between each file request, there may be an implicit |
1048 | | // pad-file request |
1049 | 0 | handle_padfile(); |
1050 | 0 | break; |
1051 | 0 | } |
1052 | | |
1053 | | // if all of the receive buffer was just consumed as chunk |
1054 | | // header, we're done |
1055 | 0 | if (recv_buffer.empty()) goto done; |
1056 | 0 | } |
1057 | 0 | } |
1058 | 0 | else |
1059 | 0 | { |
1060 | | // this is the simple case, where we don't have chunked encoding |
1061 | 0 | TORRENT_ASSERT(m_received_body <= file_req.length); |
1062 | 0 | int const copy_size = std::min(file_req.length - m_received_body |
1063 | 0 | , int(recv_buffer.size())); |
1064 | 0 | incoming_payload(recv_buffer.data(), copy_size); |
1065 | 0 | recv_buffer = recv_buffer.subspan(copy_size); |
1066 | |
|
1067 | 0 | TORRENT_ASSERT(m_received_body <= file_req.length); |
1068 | 0 | if (m_received_body == file_req.length) |
1069 | 0 | { |
1070 | | // we just completed an HTTP file request. pop it from m_file_requests |
1071 | 0 | m_file_requests.pop_front(); |
1072 | 0 | m_parser.reset(); |
1073 | 0 | m_body_start = 0; |
1074 | 0 | m_received_body = 0; |
1075 | 0 | m_chunk_pos = 0; |
1076 | 0 | m_partial_chunk_header = 0; |
1077 | | |
1078 | | // in between each file request, there may be an implicit |
1079 | | // pad-file request |
1080 | 0 | handle_padfile(); |
1081 | 0 | } |
1082 | 0 | } |
1083 | | |
1084 | 0 | if (recv_buffer.empty()) break; |
1085 | 0 | } |
1086 | 0 | done: |
1087 | | |
1088 | | // now, remove all the bytes we've processed from the receive buffer |
1089 | 0 | m_recv_buffer.cut(int(recv_buffer.data() - m_recv_buffer.get().begin()) |
1090 | 0 | , t->block_size() + request_size_overhead); |
1091 | 0 | } |
1092 | | |
1093 | | void web_peer_connection::incoming_payload(char const* buf, int len) |
1094 | 0 | { |
1095 | 0 | received_bytes(len, 0); |
1096 | 0 | m_received_body += len; |
1097 | |
|
1098 | 0 | if (is_disconnecting()) return; |
1099 | | |
1100 | | #ifndef TORRENT_DISABLE_LOGGING |
1101 | | peer_log(peer_log_alert::incoming_message, "INCOMING_PAYLOAD", "%d bytes", len); |
1102 | | #endif |
1103 | | |
1104 | | // deliver all complete bittorrent requests to the bittorrent engine |
1105 | 0 | while (len > 0) |
1106 | 0 | { |
1107 | 0 | if (m_requests.empty()) |
1108 | 0 | { |
1109 | 0 | TORRENT_ASSERT(m_piece.empty()); |
1110 | 0 | return; |
1111 | 0 | } |
1112 | | |
1113 | 0 | TORRENT_ASSERT(!m_requests.empty()); |
1114 | 0 | peer_request const& front_request = m_requests.front(); |
1115 | 0 | int const piece_size = int(m_piece.size()); |
1116 | 0 | int const copy_size = std::min(front_request.length - piece_size, len); |
1117 | | |
1118 | | // m_piece may not hold more than the response to the next BT request |
1119 | 0 | TORRENT_ASSERT(front_request.length > piece_size); |
1120 | 0 | TORRENT_ASSERT(int(m_piece.size()) == m_received_in_piece); |
1121 | | |
1122 | | // copy_size is the number of bytes we need to add to the end of m_piece |
1123 | | // to not exceed the size of the next bittorrent request to be delivered. |
1124 | | // m_piece can only hold the response for a single BT request at a time |
1125 | 0 | m_piece.resize(piece_size + copy_size); |
1126 | 0 | std::memcpy(m_piece.data() + piece_size, buf, aux::numeric_cast<std::size_t>(copy_size)); |
1127 | 0 | len -= copy_size; |
1128 | 0 | buf += copy_size; |
1129 | | |
1130 | | // keep peer stats up-to-date |
1131 | 0 | incoming_piece_fragment(copy_size); |
1132 | |
|
1133 | 0 | TORRENT_ASSERT(front_request.length >= piece_size); |
1134 | 0 | maybe_harvest_piece(); |
1135 | 0 | } |
1136 | 0 | } |
1137 | | |
1138 | | void web_peer_connection::incoming_zeroes(int len) |
1139 | 0 | { |
1140 | | #ifndef TORRENT_DISABLE_LOGGING |
1141 | | peer_log(peer_log_alert::incoming_message, "INCOMING_ZEROES", "%d bytes", len); |
1142 | | #endif |
1143 | | |
1144 | | // deliver all complete bittorrent requests to the bittorrent engine |
1145 | 0 | while (len > 0) |
1146 | 0 | { |
1147 | 0 | TORRENT_ASSERT(!m_requests.empty()); |
1148 | 0 | peer_request const& front_request = m_requests.front(); |
1149 | 0 | int const piece_size = int(m_piece.size()); |
1150 | 0 | int const copy_size = std::min(front_request.length - piece_size, len); |
1151 | | |
1152 | | // m_piece may not hold more than the response to the next BT request |
1153 | 0 | TORRENT_ASSERT(front_request.length > piece_size); |
1154 | | |
1155 | | // copy_size is the number of bytes we need to add to the end of m_piece |
1156 | | // to not exceed the size of the next bittorrent request to be delivered. |
1157 | | // m_piece can only hold the response for a single BT request at a time |
1158 | 0 | m_piece.resize(piece_size + copy_size, 0); |
1159 | 0 | len -= copy_size; |
1160 | | |
1161 | | // keep peer stats up-to-date |
1162 | 0 | incoming_piece_fragment(copy_size); |
1163 | |
|
1164 | 0 | maybe_harvest_piece(); |
1165 | 0 | } |
1166 | 0 | } |
1167 | | |
1168 | | void web_peer_connection::maybe_harvest_piece() |
1169 | 0 | { |
1170 | 0 | TORRENT_ASSERT(!m_requests.empty()); |
1171 | |
|
1172 | 0 | peer_request const& front_request = m_requests.front(); |
1173 | 0 | TORRENT_ASSERT(front_request.length >= int(m_piece.size())); |
1174 | 0 | if (int(m_piece.size()) != front_request.length) return; |
1175 | | |
1176 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
1177 | 0 | TORRENT_ASSERT(t); |
1178 | |
|
1179 | | #ifndef TORRENT_DISABLE_LOGGING |
1180 | | peer_log(peer_log_alert::incoming_message, "POP_REQUEST" |
1181 | | , "piece: %d start: %d len: %d" |
1182 | | , static_cast<int>(front_request.piece) |
1183 | | , front_request.start, front_request.length); |
1184 | | #endif |
1185 | 0 | peer_request const req = m_requests.front(); |
1186 | 0 | m_requests.pop_front(); |
1187 | |
|
1188 | 0 | incoming_piece(req, m_piece.data()); |
1189 | 0 | m_piece.clear(); |
1190 | 0 | } |
1191 | | |
1192 | | void web_peer_connection::get_specific_peer_info(peer_info& p) const |
1193 | 0 | { |
1194 | 0 | web_connection_base::get_specific_peer_info(p); |
1195 | 0 | p.flags |= peer_info::local_connection; |
1196 | 0 | p.connection_type = peer_info::web_seed; |
1197 | 0 | } |
1198 | | |
1199 | | void web_peer_connection::handle_padfile() |
1200 | 0 | { |
1201 | 0 | if (m_file_requests.empty()) return; |
1202 | 0 | if (m_requests.empty()) return; |
1203 | | |
1204 | 0 | std::shared_ptr<torrent> t = associated_torrent().lock(); |
1205 | 0 | TORRENT_ASSERT(t); |
1206 | 0 | torrent_info const& info = t->torrent_file(); |
1207 | |
|
1208 | 0 | while (!m_file_requests.empty() |
1209 | 0 | && info.orig_files().pad_file_at(m_file_requests.front().file_index)) |
1210 | 0 | { |
1211 | | // the next file is a pad file. We didn't actually send |
1212 | | // a request for this since it most likely doesn't exist on |
1213 | | // the web server anyway. Just pretend that we received a |
1214 | | // bunch of zeroes here and pop it again |
1215 | 0 | std::int64_t file_size = m_file_requests.front().length; |
1216 | | |
1217 | | // in theory the pad file can span multiple bocks, hence the loop |
1218 | 0 | while (file_size > 0) |
1219 | 0 | { |
1220 | 0 | peer_request const front_request = m_requests.front(); |
1221 | 0 | TORRENT_ASSERT(int(m_piece.size()) < front_request.length); |
1222 | |
|
1223 | 0 | int pad_size = int(std::min(file_size |
1224 | 0 | , front_request.length - std::int64_t(m_piece.size()))); |
1225 | 0 | TORRENT_ASSERT(pad_size > 0); |
1226 | 0 | file_size -= pad_size; |
1227 | |
|
1228 | 0 | incoming_zeroes(pad_size); |
1229 | |
|
1230 | | #ifndef TORRENT_DISABLE_LOGGING |
1231 | | if (should_log(peer_log_alert::info)) |
1232 | | { |
1233 | | peer_log(peer_log_alert::info, "HANDLE_PADFILE" |
1234 | | , "file: %d start: %" PRId64 " len: %d" |
1235 | | , static_cast<int>(m_file_requests.front().file_index) |
1236 | | , m_file_requests.front().start |
1237 | | , m_file_requests.front().length); |
1238 | | } |
1239 | | #endif |
1240 | 0 | } |
1241 | |
|
1242 | 0 | m_file_requests.pop_front(); |
1243 | 0 | } |
1244 | 0 | } |
1245 | | |
1246 | | } // libtorrent namespace |