/* Copyright (c) 2007-2010, 2012-2022, Arvid Norberg Copyright (c) 2016-2017, 2020-2021, Alden Torres Copyright (c) 2018, Steven Siloti All rights reserved. You may use, distribute and modify this code under the terms of the BSD license, see LICENSE file. */ #include "libtorrent/magnet_uri.hpp" #include "libtorrent/session.hpp" #include "libtorrent/aux_/escape_string.hpp" #include "libtorrent/aux_/string_util.hpp" // for to_string() #include "libtorrent/aux_/throw.hpp" #include "libtorrent/torrent_status.hpp" #include "libtorrent/torrent_info.hpp" #include "libtorrent/announce_entry.hpp" #include "libtorrent/hex.hpp" // to_hex, from_hex #include "libtorrent/aux_/socket_io.hpp" // for print_endpoint #include "libtorrent/aux_/parse_url.hpp" // for is_valid_tracker_url namespace libtorrent { #if TORRENT_ABI_VERSION < 4 std::string make_magnet_uri(torrent_handle const& handle) { if (!handle.is_valid()) return ""; std::string ret = "magnet:?"; if (handle.info_hashes().has_v1()) { ret += "xt=urn:btih:"; ret += aux::to_hex(handle.info_hashes().v1); } if (handle.info_hashes().has_v2()) { if (handle.info_hashes().has_v1()) ret += '&'; ret += "xt=urn:btmh:1220"; ret += aux::to_hex(handle.info_hashes().v2); } torrent_status st = handle.status(torrent_handle::query_name); if (!st.name.empty()) { ret += "&dn="; ret += escape_string(st.name); } for (auto const& tr : handle.trackers()) { ret += "&tr="; ret += escape_string(tr.url); } for (auto const& s : handle.url_seeds()) { ret += "&ws="; ret += escape_string(s); } return ret; } std::string make_magnet_uri(torrent_info const& info) { add_torrent_params atp; atp.info_hashes = info.info_hashes(); atp.name = info.name(); atp.trackers.reserve(info.internal_trackers().size()); for (auto const& tr : info.internal_trackers()) atp.trackers.emplace_back(tr.url); for (auto const& s : info.internal_web_seeds()) atp.url_seeds.emplace_back(s.url); return make_magnet_uri(atp); } #endif std::string make_magnet_uri(add_torrent_params const& atp) { std::string ret = "magnet:?"; bool first = true; if (atp.info_hashes.has_v1()) { ret += "xt=urn:btih:"; ret += aux::to_hex(atp.info_hashes.v1); first = false; } if (atp.info_hashes.has_v2()) { if (!first) ret += '&'; ret += "xt=urn:btmh:1220"; ret += aux::to_hex(atp.info_hashes.v2); first = false; } auto ti = atp.ti; if (first && ti) { if (ti->info_hashes().has_v1()) { ret += "xt=urn:btih:"; ret += aux::to_hex(ti->info_hashes().v1); first = false; } if (ti->info_hashes().has_v2()) { if (!first) ret += '&'; ret += "xt=urn:btmh:1220"; ret += aux::to_hex(ti->info_hashes().v2); first = false; } } #if TORRENT_ABI_VERSION < 3 if (first && !atp.info_hash.is_all_zeros()) { ret += "xt=urn:btih:"; ret += aux::to_hex(atp.info_hash); first = false; } #endif if (first) { // if we couldn't find any info-hashes, we can't make a magnet link return {}; } if (!atp.name.empty()) { ret += "&dn="; ret += escape_string(atp.name); } for (auto const& tr : atp.trackers) { ret += "&tr="; ret += escape_string(tr); } for (auto const& s : atp.url_seeds) { ret += "&ws="; ret += escape_string(s); } for (auto const& node : atp.dht_nodes) { ret += "&dht="; ret += escape_string(node.first); ret += ':'; ret += aux::to_string(node.second).data(); } for (auto const& p : atp.peers) { ret += "&x.pe="; ret += aux::print_endpoint(p); } // only include the "&so=" argument if there's at least one file // selected to not be downloaded if (std::any_of(atp.file_priorities.begin(), atp.file_priorities.end() , [](download_priority_t const p) { return p == lt::dont_download; })) { ret += "&so="; int idx = 0; bool need_comma = false; int range_start = -1; for (auto prio : atp.file_priorities) { if (prio != lt::dont_download) { if (range_start == -1) range_start = idx; } else if (range_start != -1) { if (need_comma) ret += ','; if (range_start == idx - 1) { ret += aux::to_string(range_start).data(); } else { ret += aux::to_string(range_start).data(); ret += '-'; ret += aux::to_string(idx - 1).data(); } need_comma = true; range_start = -1; } ++idx; } if (range_start != -1) { if (need_comma) ret += ','; if (range_start == idx - 1) { ret += aux::to_string(range_start).data(); } else { ret += aux::to_string(range_start).data(); ret += '-'; ret += aux::to_string(idx - 1).data(); } } } return ret; } #if TORRENT_ABI_VERSION == 1 namespace { torrent_handle add_magnet_uri_deprecated(session& ses, std::string const& uri , add_torrent_params const& p, error_code& ec) { add_torrent_params params(p); parse_magnet_uri(uri, params, ec); if (ec) return torrent_handle(); return ses.add_torrent(std::move(params), ec); } } torrent_handle add_magnet_uri(session& ses, std::string const& uri , add_torrent_params const& p, error_code& ec) { return add_magnet_uri_deprecated(ses, uri, p, ec); } #ifndef BOOST_NO_EXCEPTIONS torrent_handle add_magnet_uri(session& ses, std::string const& uri , std::string const& save_path , storage_mode_t storage_mode , bool paused , void*) { add_torrent_params params; error_code ec; parse_magnet_uri(uri, params, ec); params.storage_mode = storage_mode; params.save_path = save_path; if (paused) params.flags |= add_torrent_params::flag_paused; else params.flags &= ~add_torrent_params::flag_paused; return ses.add_torrent(std::move(params)); } torrent_handle add_magnet_uri(session& ses, std::string const& uri , add_torrent_params const& p) { error_code ec; torrent_handle ret = add_magnet_uri_deprecated(ses, uri, p, ec); if (ec) aux::throw_ex(ec); return ret; } #endif // BOOST_NO_EXCEPTIONS #endif // TORRENT_ABI_VERSION add_torrent_params parse_magnet_uri(string_view uri, error_code& ec) { add_torrent_params ret; parse_magnet_uri(uri, ret, ec); return ret; } void parse_magnet_uri(string_view uri, add_torrent_params& p, error_code& ec) { ec.clear(); std::string display_name; string_view sv(uri); if (sv.substr(0, 8) != "magnet:?"_sv) { ec = errors::unsupported_url_protocol; return; } sv = sv.substr(8); int tier = 0; bool has_ih[2] = { false, false }; while (!sv.empty()) { string_view name; std::tie(name, sv) = aux::split_string(sv, '='); string_view value; std::tie(value, sv) = aux::split_string(sv, '&'); // parameter names are allowed to have a .-suffix. // the number has no meaning, just strip it // if the characters after the period are not digits, don't strip // anything auto const [stripped_name, number] = aux::split_string(name, '.'); if (std::all_of(number.begin(), number.end(), [](char const c) { return aux::is_digit(c); } )) name = stripped_name; if (aux::string_equal_no_case(name, "dn"_sv)) // display name { error_code e; display_name = unescape_string(value, e); } else if (aux::string_equal_no_case(name, "tr"_sv)) // tracker { // since we're about to assign tiers to the trackers, make sure the two // vectors are aligned if (p.tracker_tiers.size() != p.trackers.size()) p.tracker_tiers.resize(p.trackers.size(), 0); error_code e; std::string tracker = unescape_string(value, e); if (!e && lt::aux::is_valid_tracker_url(tracker)) { #if TORRENT_USE_I2P if (!(p.flags & torrent_flags::i2p_torrent) && aux::is_i2p_url(tracker)) p.flags |= torrent_flags::i2p_torrent; #endif p.trackers.push_back(std::move(tracker)); p.tracker_tiers.push_back(tier++); } } else if (aux::string_equal_no_case(name, "ws"_sv)) // web seed { error_code e; std::string webseed = unescape_string(value, e); if (!e) p.url_seeds.push_back(std::move(webseed)); } else if (aux::string_equal_no_case(name, "xt"_sv)) { std::string unescaped_btih; if (value.find('%') != string_view::npos) { unescaped_btih = unescape_string(value, ec); if (ec) return; value = unescaped_btih; } if (value.substr(0, 9) == "urn:btih:") { value = value.substr(9); sha1_hash info_hash; if (value.size() == 40) { if (!aux::from_hex(value, info_hash.data())) { ec = errors::invalid_info_hash; return; } } else if (value.size() == 32) { std::string const ih = base32decode(value); if (ih.size() != 20) { ec = errors::invalid_info_hash; return; } info_hash.assign(ih); } else { ec = errors::invalid_info_hash; return; } p.info_hashes.v1 = info_hash; has_ih[0] = true; } else if (value.substr(0, 9) == "urn:btmh:") { value = value.substr(9); // hash must be sha256 if (value.substr(0, 4) != "1220") { ec = errors::invalid_info_hash; return; } value = value.substr(4); if (value.size() != 64) { ec = errors::invalid_info_hash; return; } if (!aux::from_hex(value, p.info_hashes.v2.data())) { ec = errors::invalid_info_hash; return; } has_ih[1] = true; } } else if (aux::string_equal_no_case(name, "so"_sv)) // select-only (files) { // accept only digits, '-' and ',' if (std::any_of(value.begin(), value.end(), [](char c) { return !aux::is_digit(c) && c != '-' && c != ','; })) continue; // make sure all file priorities are set to 0, except the ones // we specify in the file_priorities p.flags |= torrent_flags::default_dont_download; do { string_view token; std::tie(token, value) = aux::split_string(value, ','); if (token.empty()) continue; int idx1, idx2; // TODO: what's the right number here? constexpr int max_index = 10000; // can't risk out of memory auto const divider = token.find_first_of('-'); if (divider != std::string::npos) // it's a range { if (divider == 0) // no start index continue; if (divider == token.size() - 1) // no end index continue; auto const first = aux::parse_decimal(token.substr(0, divider)); if (!first || *first < 0 || *first > max_index) // invalid index continue; auto const last = aux::parse_decimal(token.substr(divider + 1)); if (!last || *last < 0 || *last > max_index) // invalid index continue; idx1 = *first; idx2 = *last; if (idx1 > idx2) // wrong range limits continue; } else // it's an index { auto const idx = aux::parse_decimal(token); if (!idx || *idx < 0 || *idx > max_index) // invalid index continue; idx1 = *idx; idx2 = idx1; } if (int(p.file_priorities.size()) <= idx2) p.file_priorities.resize(static_cast(idx2) + 1, dont_download); for (int i = idx1; i <= idx2; i++) p.file_priorities[std::size_t(i)] = default_priority; } while (!value.empty()); } else if (aux::string_equal_no_case(name, "x.pe"_sv)) { error_code e; tcp::endpoint endp = aux::parse_endpoint(value, e); if (!e) p.peers.push_back(std::move(endp)); } #ifndef TORRENT_DISABLE_DHT else if (aux::string_equal_no_case(name, "dht"_sv)) { auto const divider = value.find_last_of(':'); if (divider != std::string::npos) { auto const port = aux::parse_decimal(value.substr(divider + 1)); if (port && *port > 0 && *port < int(std::numeric_limits::max())) p.dht_nodes.emplace_back(value.substr(0, divider), *port); } } #endif } if (!has_ih[0] && !has_ih[1]) { ec = errors::missing_info_hash_in_uri; return; } #if TORRENT_ABI_VERSION < 3 p.info_hash = p.info_hashes.get_best(); #endif if (!display_name.empty()) p.name = display_name; } add_torrent_params parse_magnet_uri(string_view uri) { error_code ec; add_torrent_params ret; parse_magnet_uri(uri, ret, ec); if (ec) aux::throw_ex(ec); return ret; } }