diff --git a/CMakeLists.txt b/CMakeLists.txt index 2078c51a73a..73f333200a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ set(libtorrent_aux_include_files bandwidth_socket.hpp bencoder.hpp bind_to_device.hpp + bitmask.hpp bloom_filter.hpp bt_peer_connection.hpp buffer.hpp @@ -156,6 +157,13 @@ set(libtorrent_aux_include_files copy_ptr.hpp cpuid.hpp crc32c.hpp + curl.hpp + curl_basic_request.hpp + curl_boost_socket.hpp + curl_pool.hpp + curl_request.hpp + curl_tracker_manager.hpp + curl_tracker_request.hpp deadline_timer.hpp debug.hpp debug_disk_thread.hpp @@ -192,7 +200,9 @@ set(libtorrent_aux_include_files http_parser.hpp http_stream.hpp http_tracker_connection.hpp + http_tracker_request.hpp instantiate_connection.hpp + intrusive_list.hpp invariant_check.hpp io.hpp io_bytes.hpp @@ -203,6 +213,7 @@ set(libtorrent_aux_include_files link.hpp listen_socket_handle.hpp lsd.hpp + memory.hpp merkle.hpp merkle_tree.hpp mmap_storage.hpp @@ -325,6 +336,12 @@ set(sources cpuid.cpp crc32c.cpp create_torrent.cpp + curl_basic_request.cpp + curl_boost_socket.cpp + curl_pool.cpp + curl_request.cpp + curl_tracker_manager.cpp + curl_tracker_request.cpp directory.cpp disabled_disk_io.cpp disk_buffer_holder.cpp @@ -352,6 +369,7 @@ set(sources http_connection.cpp http_parser.cpp http_tracker_connection.cpp + http_tracker_request.cpp i2p_stream.cpp identify_client.cpp instantiate_connection.cpp @@ -738,6 +756,8 @@ target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME i2p DE DESCRIPTION "build with I2P support" DISABLED TORRENT_USE_I2P=0) target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME webtorrent DEFAULT OFF DESCRIPTION "build with WebTorrent support" DISABLED TORRENT_USE_RTC=0) +target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME curl DEFAULT OFF + DESCRIPTION "Enable support for connection reuse and advanced HTTP features towards trackers" ENABLED TORRENT_USE_CURL=1) target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME logging DEFAULT ON DESCRIPTION "build with logging" DISABLED TORRENT_DISABLE_LOGGING) target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME mutable-torrents DEFAULT ON @@ -858,6 +878,21 @@ if (webtorrent) endif() endif() +if (curl) + find_public_dependency(CURL REQUIRED) + set_package_properties(CURL + PROPERTIES + URL "https://curl.se/" + DESCRIPTION "Command line tool and library for transferring data with URLs" + TYPE RECOMMENDED + PURPOSE "Enable support for connection reuse and advanced HTTP features towards tracker" + ) + target_link_libraries(torrent-rasterbar PUBLIC CURL::libcurl) + + target_optional_compile_definitions(torrent-rasterbar PUBLIC FEATURE NAME curl_debug DEFAULT OFF + DESCRIPTION "Enable curl logging connection details to stderr" ENABLED TORRENT_DEBUG_LIBCURL=1) +endif() + # Boost find_public_dependency(Boost REQUIRED) target_link_libraries(torrent-rasterbar PUBLIC Boost::headers) diff --git a/ChangeLog b/ChangeLog index c350c0fe527..d72135a533f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,6 @@ 2.1.0 not released + * (opt-in) use curl for tracker communication * make the save path for part files configurable * retry failed SAM connection (for i2p) * deprecated remap_files(), and prevent it from breaking v2 torrents diff --git a/Jamfile b/Jamfile index 55b0602bd1c..ba6a3ff173e 100644 --- a/Jamfile +++ b/Jamfile @@ -109,6 +109,11 @@ rule linking ( properties * ) result += wolfssl ; } + if on in $(properties) + { + result += curl ; + } + if windows in $(properties) || cygwin in $(properties) { @@ -608,6 +613,26 @@ rule wolfssl-include-path ( properties * ) return $(result) ; } +rule curl-lib-path ( properties * ) +{ + local CURL_LIB = [ feature.get-values : $(properties) ] ; + if $(CURL_LIB) != "" + { + return $(CURL_LIB) ; + } + return ; +} + +rule curl-include-path ( properties * ) +{ + local CURL_INCLUDE = [ feature.get-values : $(properties) ] ; + if $(CURL_INCLUDE) != "" + { + return $(CURL_INCLUDE) ; + } + return ; +} + path-constant blacklist-file : tools/sanitizer-blacklist.txt ; feature openssl-lib : : free path ; @@ -642,6 +667,17 @@ feature webtorrent : off on : composite propagated ; feature.compose on : TORRENT_USE_RTC=1 ; feature.compose off : TORRENT_USE_RTC=0 ; +feature curl : off on : composite propagated ; +feature.compose on : TORRENT_USE_CURL=1 ; +feature.compose off : TORRENT_USE_CURL=0 ; + +feature curl-lib : : free path ; +feature curl-include : : free path ; + +feature curl_debug : off on : composite propagated ; +feature.compose on : TORRENT_DEBUG_LIBCURL=1 ; +feature.compose off : TORRENT_DEBUG_LIBCURL=0 ; + feature asserts : off on production system : composite propagated ; feature.compose on : TORRENT_USE_ASSERTS=1 ; feature.compose production : TORRENT_USE_ASSERTS=1 TORRENT_PRODUCTION_ASSERTS=1 ; @@ -816,6 +852,9 @@ lib gnutls : : gnutls @gnutls-lib-path : : lib wolfssl : : wolfssl @wolfssl-lib-path : : @wolfssl-include-path ; +lib curl : : curl @curl-lib-path : : + @curl-include-path ; + lib dbghelp : : dbghelp ; # required for networking on beos @@ -853,6 +892,12 @@ SOURCES = cpuid crc32c create_torrent + curl_basic_request + curl_boost_socket + curl_pool + curl_request + curl_tracker_manager + curl_tracker_request directory disk_buffer_holder disk_buffer_pool @@ -928,6 +973,7 @@ SOURCES = time tracker_manager http_tracker_connection + http_tracker_request udp_tracker_connection timestamp_history udp_socket diff --git a/Makefile b/Makefile index aee52318631..bb866c83509 100644 --- a/Makefile +++ b/Makefile @@ -319,6 +319,12 @@ SOURCES = \ cpuid.cpp \ crc32c.cpp \ create_torrent.cpp \ + curl_basic_request.cpp \ + curl_boost_socket.cpp \ + curl_pool.cpp \ + curl_request.cpp \ + curl_tracker_manager.cpp \ + curl_tracker_request.cpp \ directory.cpp \ disabled_disk_io.cpp \ disk_buffer_holder.cpp \ @@ -346,6 +352,7 @@ SOURCES = \ http_connection.cpp \ http_parser.cpp \ http_tracker_connection.cpp \ + http_tracker_request.cpp \ i2p_stream.cpp \ identify_client.cpp \ instantiate_connection.cpp \ @@ -541,6 +548,7 @@ HEADERS = \ aux_/bandwidth_socket.hpp \ aux_/bencoder.hpp \ aux_/bind_to_device.hpp \ + aux_/bitmask.hpp \ aux_/buffer.hpp \ aux_/byteswap.hpp \ aux_/bloom_filter.hpp \ @@ -551,6 +559,13 @@ HEADERS = \ aux_/copy_ptr.hpp \ aux_/cpuid.hpp \ aux_/crc32c.hpp \ + aux_/curl.hpp \ + aux_/curl_basic_request.hpp \ + aux_/curl_boost_socket.hpp \ + aux_/curl_pool.hpp \ + aux_/curl_request.hpp \ + aux_/curl_tracker_manager.hpp \ + aux_/curl_tracker_request.hpp \ aux_/deadline_timer.hpp \ aux_/debug.hpp \ aux_/debug_disk_thread.hpp \ @@ -589,7 +604,9 @@ HEADERS = \ aux_/http_parser.hpp \ aux_/http_stream.hpp \ aux_/http_tracker_connection.hpp \ + aux_/http_tracker_request.hpp \ aux_/instantiate_connection.hpp \ + aux_/intrusive_list.hpp \ aux_/invariant_check.hpp \ aux_/io.hpp \ aux_/io_bytes.hpp \ @@ -600,6 +617,7 @@ HEADERS = \ aux_/link.hpp \ aux_/listen_socket_handle.hpp \ aux_/lsd.hpp \ + aux_/memory.hpp \ aux_/merkle.hpp \ aux_/merkle_tree.hpp \ aux_/mmap.hpp \ diff --git a/docs/building.rst b/docs/building.rst index 8e9f94e269c..b3f2b900788 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -275,6 +275,10 @@ To customize the library path and include path for wolfSSL, set the features To disable linking against any SSL library, set the ``crypto`` build feature to ``built-in``. This will use an embedded version if SHA-1. +When building with curl enabled, customize the library and include paths by +setting the features ``curl-lib`` and ``curl-include``, respectively. To enable +logging of curl's connection details to stderr, set ``curl_debug=on``. + Build features ~~~~~~~~~~~~~~ @@ -460,6 +464,15 @@ Build features | | * ``off`` - disable mmap storage, and fall back to | | | single-threaded, portable file operations. | +--------------------------+----------------------------------------------------+ +| ``curl`` | * ``off`` - default. HTTP connections to trackers | +| | don't support connection- and SSL session reuse. | +| | * ``on`` - libcurl is used for HTTP connections | +| | to trackers, enabling connection- and SSL | +| | session reuse. This includes support for the | +| | default libcurl HTTP features. | +| | Note that libcurl needs to be build against | +| | c-aris for async DNS support. | ++--------------------------+----------------------------------------------------+ The ``variant`` feature is *implicit*, which means you don't need to specify the name of the feature, just the value. @@ -599,6 +612,10 @@ Other build options are: Options are set on the ``cmake`` command line with the ``-D`` option or later on using ``ccmake`` or ``cmake-gui`` applications. ``cmake`` run outputs a summary of all available options and their current values. +To link with a custom build of libcurl (or any other library), first build and install libcurl with +custom dependencies to a custom prefix path. Then use the option ``CMAKE_PREFIX_PATH=/path/to/custom/dependencies/prefix`` +in libtorrent's cmake configuration. + Step 2: Building libtorrent ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -748,6 +765,10 @@ own code that compiles and links with libtorrent. | | peers are connected over authenticated SSL | | | streams. | +----------------------------------------+-------------------------------------------------+ +| ``TORRENT_USE_CURL=1`` | Libcurl is used for HTTP connections to | +| | trackers. This will enable connection- and SSL | +| | session reuse. | ++----------------------------------------+-------------------------------------------------+ .. _`BEP 38`: https://www.bittorrent.org/beps/bep_0038.html diff --git a/include/libtorrent/aux_/bitmask.hpp b/include/libtorrent/aux_/bitmask.hpp new file mode 100644 index 00000000000..b02b8d5863b --- /dev/null +++ b/include/libtorrent/aux_/bitmask.hpp @@ -0,0 +1,43 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_BITMASK_HPP_INCLUDED +#define TORRENT_BITMASK_HPP_INCLUDED + +#include + +template +class bitmask { + using U = std::underlying_type_t; + U bits; +public: + static_assert(std::is_enum_v); + using option = E; + + constexpr bitmask(E e = static_cast(0)) noexcept : bits(static_cast(e)) {} + constexpr explicit bitmask(U b) noexcept : bits(b) {} + [[nodiscard]] constexpr U raw() const noexcept { return bits; } + + constexpr bool operator==(bitmask o) const noexcept { return bits == o.bits; } + constexpr bitmask operator|(bitmask o) const noexcept { return bitmask(bits | o.bits); } + constexpr bitmask operator&(bitmask o) const noexcept { return bitmask(bits & o.bits); } + constexpr bitmask operator^(bitmask o) const noexcept { return bitmask(bits ^ o.bits); } + constexpr bitmask operator~() const noexcept { return bitmask(~bits); } + constexpr bitmask& operator|=(bitmask o) noexcept { bits |= o.bits; return *this; } + constexpr bitmask& operator&=(bitmask o) noexcept { bits &= o.bits; return *this; } + constexpr bitmask& operator^=(bitmask o) noexcept { bits ^= o.bits; return *this; } + + constexpr explicit operator bool() const noexcept { return bits != 0; } + [[nodiscard]] constexpr bool test(E e) const noexcept { return (bits & static_cast(e)) != 0; } + [[nodiscard]] constexpr bool test(bitmask o) const noexcept { return (bits & o.bits) != 0; } + constexpr void unset(E e) noexcept { bits &= ~static_cast(e); } +}; + +#endif //TORRENT_BITMASK_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl.hpp b/include/libtorrent/aux_/curl.hpp new file mode 100644 index 00000000000..0d7754b61c8 --- /dev/null +++ b/include/libtorrent/aux_/curl.hpp @@ -0,0 +1,58 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_HPP_INCLUDED +#define TORRENT_CURL_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include +#include +#include + +namespace libtorrent::aux { + +enum class curl_poll_t : int { + none = CURL_POLL_NONE, + in = CURL_POLL_IN, + out = CURL_POLL_OUT, + remove = CURL_POLL_REMOVE, +}; +static_assert(CURL_POLL_INOUT == (CURL_POLL_IN | CURL_POLL_OUT)); + +enum class curl_cselect_t : int { + none = 0, // curl allows the value to be 0, but does not define a type for it + in = CURL_CSELECT_IN, + out = CURL_CSELECT_OUT, + err = CURL_CSELECT_ERR, +}; + +class curl_easy_error: public std::runtime_error +{ + CURLcode code_; + +public: + curl_easy_error( CURLcode const ec, std::string const & prefix ): + std::runtime_error( prefix + ": " + curl_easy_strerror(ec) ), code_( ec ) {} + + [[nodiscard]] CURLcode code() const noexcept + { + return code_; + } +}; + +inline bool curl_version_lower_than(unsigned int version) { + const auto ver = curl_version_info(CURLVERSION_NOW); + return !ver || ver->version_num < version; +} +} + +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl_basic_request.hpp b/include/libtorrent/aux_/curl_basic_request.hpp new file mode 100644 index 00000000000..08edc33cb46 --- /dev/null +++ b/include/libtorrent/aux_/curl_basic_request.hpp @@ -0,0 +1,105 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_BASIC_REQUEST_HPP_INCLUDED +#define TORRENT_CURL_BASIC_REQUEST_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include +#include "libtorrent/address.hpp" +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/memory.hpp" +#include "libtorrent/aux_/throw.hpp" +#include "libtorrent/error_code.hpp" + +namespace libtorrent::aux { +struct proxy_settings; + +// Convenience/type-safe wrapper around Curl_easy for curl's built-in features +class TORRENT_EXTRA_EXPORT curl_basic_request { +public: + curl_basic_request(); + + void set_defaults(); + + template + [[nodiscard]] static T* from_handle(CURL* easy_handle) + { + if (!easy_handle) + return nullptr; + T* ptr = nullptr; + const auto error = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &ptr); + if (error) + throw_ex(error, "curl_easy_getinfo (CURLOPT_PRIVATE)"); + return ptr; + } + + [[nodiscard]] CURL* handle() const noexcept { return m_curl_handle.get(); } + + [[nodiscard]] errors::http_errors http_status() const; + [[nodiscard]] address get_ip(error_code& ec) const; + + [[nodiscard]] bool bind(const std::string& device, const address& local_address); + void set_proxy(const proxy_settings& ps, bool verify_ssl); + + void set_user_agent(const std::string& s); + void set_url(const std::string& s); + void set_ipresolve(long option); + + void set_ssl_verify_host(bool onoff); + void set_ssl_verify_peer(bool onoff); + + void set_pipewait(bool onoff); + + void set_userpwd(const std::string& s); + void clear_userpwd(); + + [[nodiscard]] const char* get_url() const; + [[nodiscard]] const char* get_redirect_url() const; + [[nodiscard]] long get_num_connects() const; + [[nodiscard]] std::size_t get_header_size() const; + [[nodiscard]] std::size_t get_compressed_body_size() const; + + // includes bytes from redirection requests + [[nodiscard]] std::size_t get_request_size() const; + + void set_write_callback(curl_write_callback cb); + void set_write_callback_data(void* data); + +protected: + void set_private_data(void* obj); + void set_debug_logging(bool onoff); + + void set_opensocket_callback(curl_opensocket_callback cb); + void set_opensocket_callback_data(void* data); + + void set_prereq_callback(curl_prereq_callback cb); + void set_prereq_callback_data(void* data); + + void set_resolver_callback(curl_resolver_start_callback cb); + void set_resolver_callback_data(void* data); + +private: + template + void setopt(bool value); + + template + void setopt(const T& value); + + template + [[nodiscard]] T getopt() const; + + const unique_ptr_with_deleter m_curl_handle; +}; +} + +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_BASIC_REQUEST_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl_boost_socket.hpp b/include/libtorrent/aux_/curl_boost_socket.hpp new file mode 100644 index 00000000000..fdbe0929014 --- /dev/null +++ b/include/libtorrent/aux_/curl_boost_socket.hpp @@ -0,0 +1,64 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_BOOST_SOCKET_HPP_INCLUDED +#define TORRENT_CURL_BOOST_SOCKET_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include "libtorrent/aux_/bitmask.hpp" +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/intrusive_list.hpp" +#include "libtorrent/aux_/memory.hpp" +#include "libtorrent/error_code.hpp" + +#if defined TORRENT_WINDOWS || defined TORRENT_CYGWIN +#include +#else +#include +#endif + +namespace libtorrent::aux { +class curl_pool; + +class curl_boost_socket : public unique_ptr_intrusive_list_base { +public: +#if defined TORRENT_WINDOWS || defined TORRENT_CYGWIN + using socket_t = boost::asio::windows::stream_handle; +#else + using socket_t = boost::asio::posix::stream_descriptor; +#endif + + curl_boost_socket(curl_pool& pool, socket_t&& socket) + : m_pool(pool) + , m_socket(std::move(socket)) + { + } + + void set_poll_mode(bitmask new_poll_mode); + + // releases the native_handle without closing it + void release_handle() { m_socket.release(); } + [[nodiscard]] curl_socket_t native_handle() { return m_socket.native_handle(); } + + [[nodiscard]] static std::unique_ptr wrap(curl_pool& pool, curl_socket_t native_socket, error_code& ec); +private: + void subscribe_read(); + void subscribe_write(); + + // allows async completion-handlers to notify pool through the "this" pointer + curl_pool& m_pool; + + socket_t m_socket; + bitmask m_poll_mode = curl_poll_t::none; +}; +} +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_BOOST_SOCKET_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl_pool.hpp b/include/libtorrent/aux_/curl_pool.hpp new file mode 100644 index 00000000000..167acc9b3e9 --- /dev/null +++ b/include/libtorrent/aux_/curl_pool.hpp @@ -0,0 +1,93 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_POOL_HPP_INCLUDED +#define TORRENT_CURL_POOL_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include +#include +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/curl_boost_socket.hpp" +#include "libtorrent/aux_/deadline_timer.hpp" +#include "libtorrent/aux_/intrusive_list.hpp" +#include "libtorrent/io_context.hpp" + +namespace libtorrent::aux { +class curl_request; + +class TORRENT_EXTRA_EXPORT curl_pool { +public: + using executor_type = boost::asio::any_io_executor; + + curl_pool(const executor_type& executor); + ~curl_pool(); + + // Triggers curl processing of socket event. + // May call destructor of the "socket" parameter and other sockets before it returns. + // May call completion handler for easy handles before it returns (which may delete or remove them). + void socket_event(curl_boost_socket& socket, curl_cselect_t event); + + // Default argument CURL_SOCKET_TIMEOUT is used for timeouts. It can also be used to kickstart curl processing but + // curl should already create timeouts of zero seconds when it wants to process something. + // May call destructor of sockets before it returns + // May call completion handler for easy handles before it returns (which may delete or remove them) + void process_socket_action(curl_socket_t native_socket = CURL_SOCKET_TIMEOUT, curl_cselect_t event = curl_cselect_t::none); + + void add_request(curl_request&); + void remove_request(curl_request&); + + void set_max_connections(int max_connections); + void set_max_host_connections(long value); + + using completion_handler_t = std::function; + // request is removed from the pool before completion handler is called, callee should not attempt to remove it. + void set_completion_callback(completion_handler_t cb) { m_completion_handler = std::move(cb); } + + void on_request_timeout(curl_request& crequest); + [[nodiscard]] executor_type get_executor() const noexcept { return m_executor; } +private: + template + void setopt(T value); + + int set_timeout(long timeout_ms) noexcept; + + static int update_socket_shim(CURL* /* easy_handle */, + curl_socket_t native_socket, + int what, + void* clientp, + void* socketp); + + static int update_socket(curl_socket_t native_socket, + bitmask poll_mode, + curl_pool* pool, + curl_boost_socket* socket); + + [[nodiscard]] CURLM* handle() const noexcept { return m_curl_handle; } + + // May call completion handler for easy handles before it returns + void process_completed_requests(); + + curl_boost_socket& add_socket(std::unique_ptr socket) noexcept; + void remove_socket(curl_boost_socket& socket) noexcept; + + // raw_ptr due to special destructor requirements + CURLM* m_curl_handle; + + using request_list_t = unique_ptr_intrusive_list; + request_list_t m_sockets; + completion_handler_t m_completion_handler; + deadline_timer m_timer; + executor_type m_executor; +}; +} +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_POOL_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl_request.hpp b/include/libtorrent/aux_/curl_request.hpp new file mode 100644 index 00000000000..2e13dae7f7b --- /dev/null +++ b/include/libtorrent/aux_/curl_request.hpp @@ -0,0 +1,112 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_REQUEST_HPP_INCLUDED +#define TORRENT_CURL_REQUEST_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include +#include +#include "libtorrent/address.hpp" +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/curl_basic_request.hpp" +#include "libtorrent/aux_/deadline_timer.hpp" +#include "libtorrent/error_code.hpp" +#include "libtorrent/io_context.hpp" +#include "libtorrent/span.hpp" +#include "libtorrent/time.hpp" +#include + +namespace libtorrent::aux { +class exploded_url; + +// extends curl_basic_request with features not natively supported by curl +class TORRENT_EXTRA_EXPORT curl_request : public curl_basic_request { +public: + struct error_type { + error_code code; + operation_t op = operation_t::unknown; + std::string message = {}; + constexpr explicit operator bool() const noexcept { return static_cast(code); } + }; + + [[nodiscard]] static curl_request* from_handle(CURL* easy_handle) + { + return curl_basic_request::from_handle(easy_handle); + } + + explicit curl_request(std::size_t max_buffer_size, const io_context::executor_type& executor); + ~curl_request() = default; + + void set_defaults(); + + [[nodiscard]] span data() const noexcept; + [[nodiscard]] error_type get_error(CURLcode result) const; + + using filter_t = bool (*)(curl_request& request, const address& ip); + void set_filter(filter_t filter) noexcept { m_filter = filter; } + + using redirect_t = error_code (*)(curl_request& request, const exploded_url& parts); + void set_redirect_callback(redirect_t cb) noexcept { m_redirect_cb = cb; } + + using timeout_callback_t = std::function; + // callback is called async, request object can be deleted inside the callback + void set_timeout_callback(timeout_callback_t cb) noexcept { m_timeout_callback = cb; } + void set_timeout(seconds32 timeout) { m_timeout = timeout; } + void cancel_timeout() { m_timer.cancel(); }; + + // returns false on error + bool prepare_to_follow_redirect(); + + [[nodiscard]] bool is_redirect_response() const; + void set_max_redirects(int value) { m_allowed_redirects = value; } + + void* get_userdata() const { return m_userdata; } + void set_userdata(void* value) { m_userdata = value; } + +private: + void start_timeout(seconds32 timeout); + + [[nodiscard]] error_type redirect_security_settings(const exploded_url& parts); + + [[nodiscard]] bool filter(const address& ep); + + static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + + static curl_socket_t opensocket(void* clientp, + curlsocktype purpose, + curl_sockaddr* addr); + + static curl_socket_t before_curl_request(void* clientp, + char* conn_primary_ip, + char* conn_local_ip, + int conn_primary_port, + int conn_local_port); + + static int before_resolving(void *resolver_state, void *reserved, void *userdata); + + timeout_callback_t m_timeout_callback = nullptr; + filter_t m_filter = nullptr; + redirect_t m_redirect_cb = nullptr; + void* m_userdata = nullptr; + + boost::beast::flat_buffer m_read_buffer; + deadline_timer m_timer; + seconds32 m_timeout{}; + address m_filter_allowed; + error_type m_error; + + int m_allowed_redirects = 0; +}; +} + +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_REQUEST_HPP_INCLUDED diff --git a/include/libtorrent/aux_/curl_tracker_manager.hpp b/include/libtorrent/aux_/curl_tracker_manager.hpp new file mode 100644 index 00000000000..2abd1b5e289 --- /dev/null +++ b/include/libtorrent/aux_/curl_tracker_manager.hpp @@ -0,0 +1,77 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef LIBTORRENT_CURL_REQUEST_MANAGER_HPP +#define LIBTORRENT_CURL_REQUEST_MANAGER_HPP +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/curl_tracker_request.hpp" +#include "libtorrent/aux_/intrusive_list.hpp" +#include "libtorrent/aux_/memory.hpp" +#include "libtorrent/io_context.hpp" + +namespace libtorrent::aux { +class curl_pool; +class curl_tracker_request; +struct session_settings; +struct request_callback; +class tracker_manager; +struct tracker_request; + +struct TORRENT_EXTRA_EXPORT curl_global_initializer { + curl_global_initializer(); + ~curl_global_initializer(); +}; + +class curl_tracker_manager { +public: + explicit curl_tracker_manager(tracker_manager& general_manager); + + ~curl_tracker_manager(); + curl_tracker_manager(curl_tracker_manager const&) = delete; + + curl_tracker_manager& operator=(curl_tracker_manager const&) = delete; + + // the request callback shall not be called before this function returns + void add(io_context& ios, + tracker_request&& req, + std::weak_ptr cb); + + // note: stopped events notify the tracker that this client is no longer an active peer + void abort_all(bool abort_stopped_events = false); + + [[nodiscard]] int size() const noexcept { return static_cast(m_requests.size()); } + [[nodiscard]] bool empty() const noexcept { return m_requests.empty(); } + + void received_bytes(int bytes); + void sent_bytes(int bytes); + + [[nodiscard]] session_settings const& settings() const; + +private: + // two-step initialization because it needs an executor + void initialize_pool(io_context& ios); + void on_completed(curl_request& request, CURLcode result); + + // constructed first, destructed last + curl_global_initializer m_curl_initializer; + + using request_list_t = unique_ptr_intrusive_list; + request_list_t m_requests; + + std::unique_ptr m_pool; + + tracker_manager& m_general_manager; +}; +} +#endif //TORRENT_USE_CURL +#endif //LIBTORRENT_CURL_REQUEST_MANAGER_HPP diff --git a/include/libtorrent/aux_/curl_tracker_request.hpp b/include/libtorrent/aux_/curl_tracker_request.hpp new file mode 100644 index 00000000000..f614ba471c5 --- /dev/null +++ b/include/libtorrent/aux_/curl_tracker_request.hpp @@ -0,0 +1,91 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_CURL_TRACKER_REQUEST_HPP_INCLUDED +#define TORRENT_CURL_TRACKER_REQUEST_HPP_INCLUDED +#include "libtorrent/config.hpp" + +#if TORRENT_USE_CURL +#include "libtorrent/aux_/curl.hpp" +#include "libtorrent/aux_/curl_request.hpp" +#include "libtorrent/aux_/intrusive_list.hpp" +#include "libtorrent/aux_/http_tracker_request.hpp" +#include "libtorrent/error_code.hpp" +#include "libtorrent/io_context.hpp" +#include "libtorrent/time.hpp" + +namespace libtorrent::aux { +class curl_tracker_manager; +struct request_callback; +struct tracker_request; + +class curl_tracker_request : public unique_ptr_intrusive_list_base { +public: + using error_type = http_tracker_request::error_type; + + curl_tracker_request( + curl_tracker_manager& owner, + tracker_request&& req, + std::weak_ptr c, + const io_context::executor_type& executor); + + curl_tracker_request(const curl_tracker_request&) = delete; + + [[nodiscard]] static curl_tracker_request& from_request(curl_request& request) + { + auto r = static_cast(request.get_userdata()); + TORRENT_ASSERT(r); + return *r; + } + + [[nodiscard]] error_type initialize_request(); + +#ifndef TORRENT_DISABLE_LOGGING + [[nodiscard]] std::shared_ptr requester() const { return m_callback.lock(); } +#endif + + void complete(CURLcode result); + void fail(const error_type& info) { fail(info.code, info.op, info.failure_reason, info.interval, info.min_interval); } + + [[nodiscard]] curl_request& get_curl_request() noexcept { return m_request; } + [[nodiscard]] const curl_request & get_curl_request() const noexcept { return m_request; } + + [[nodiscard]] const tracker_request& get_params() const noexcept { return *m_params; } + [[nodiscard]] bool is_stopped_event() const noexcept; + + [[nodiscard]] curl_tracker_manager& get_owner() const noexcept { return m_owner; } +private: + void fail(const error_code& ec, + operation_t op = operation_t::unknown, + const std::string& message = {}, + seconds32 interval = {}, + seconds32 min_interval = {}); + + void on_response(); + + [[nodiscard]] error_type filter_hostname(string_view hostname); + [[nodiscard]] error_code on_redirect(const exploded_url& parts); + [[nodiscard]] static error_code on_redirect(curl_request& request, const exploded_url& parts); + [[nodiscard]] bool on_filter(const address& ip); + [[nodiscard]] static bool on_filter(curl_request&, const address& ip); + void track_statistics(); + + // storing the entire tracker_request object should not be necessary + // unique_ptr to prevent circular header includes + const std::unique_ptr m_params; + + curl_tracker_manager& m_owner; + curl_request m_request; + std::weak_ptr m_callback; + error_type m_error; +}; +} +#endif //TORRENT_USE_CURL +#endif //TORRENT_CURL_TRACKER_REQUEST_HPP_INCLUDED diff --git a/include/libtorrent/aux_/http_tracker_connection.hpp b/include/libtorrent/aux_/http_tracker_connection.hpp index a27b3fd6b06..2220c9a143e 100644 --- a/include/libtorrent/aux_/http_tracker_connection.hpp +++ b/include/libtorrent/aux_/http_tracker_connection.hpp @@ -1,6 +1,6 @@ /* -Copyright (c) 2004, 2006-2009, 2012, 2014-2017, 2019-2021, Arvid Norberg +Copyright (c) 2004, 2006-2009, 2012, 2014-2017, 2019-2026 Arvid Norberg Copyright (c) 2016, 2020-2021, Alden Torres All rights reserved. @@ -18,9 +18,9 @@ see LICENSE file. #include "libtorrent/peer_id.hpp" #include "libtorrent/error_code.hpp" #include "libtorrent/aux_/tracker_manager.hpp" // for tracker_connection +#include "libtorrent/aux_/http_tracker_request.hpp" namespace libtorrent { - struct bdecode_node; } @@ -45,6 +45,8 @@ namespace libtorrent::aux { void start() override; void close() override; + void fail_error(http_tracker_request::error_type& error); + private: std::shared_ptr shared_from_this() diff --git a/include/libtorrent/aux_/http_tracker_request.hpp b/include/libtorrent/aux_/http_tracker_request.hpp new file mode 100644 index 00000000000..42fc1fb12ad --- /dev/null +++ b/include/libtorrent/aux_/http_tracker_request.hpp @@ -0,0 +1,84 @@ +/* + +Copyright (c) 2004, 2006-2009, 2012, 2014-2017, 2019-2026 Arvid Norberg +Copyright (c) 2016, 2020-2021, Alden Torres +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_HTTP_TRACKER_REQUEST_HPP_INCLUDED +#define TORRENT_HTTP_TRACKER_REQUEST_HPP_INCLUDED + +#include +#include "libtorrent/address.hpp" +#include "libtorrent/error_code.hpp" +#include "libtorrent/operations.hpp" +#include "libtorrent/socket.hpp" +#include "libtorrent/span.hpp" +#include "libtorrent/time.hpp" + +namespace libtorrent::aux { +struct request_callback; +struct tracker_request; +struct session_settings; + +class http_tracker_request { + const tracker_request& m_params; + const session_settings& m_settings; +public: + http_tracker_request(const tracker_request& req, const session_settings& settings) + : m_params(req), m_settings(settings) + { + } + + constexpr static int max_redirects = 5; + + struct error_type{ + error_code code; + operation_t op = operation_t::bittorrent; + std::string failure_reason = {}; + seconds32 interval{}; + seconds32 min_interval{}; + explicit operator bool() const noexcept { return static_cast(code); } + }; + + [[nodiscard]] error_type process_response( + request_callback& cb, + const address& tracker_ip, + const std::list
& ip_list, + span data) const; + + [[nodiscard]] std::string build_tracker_url(bool i2p, error_type& error) const; + + [[nodiscard]] std::string get_user_agent() const; + [[nodiscard]] seconds32 get_timeout() const; + +#ifndef TORRENT_DISABLE_LOGGING + void log_request(request_callback& cb, const std::string& url) const; +#endif + + [[nodiscard]] error_type validate_socket(bool i2p) const; + [[nodiscard]] bool allowed_by_idna(string_view hostname) const; + + [[nodiscard]] error_type filter( + std::weak_ptr& logger, + std::vector& endpoints, + string_view url) const; + + [[nodiscard]] error_type filter( + std::weak_ptr& logger, + const address& ip, + string_view url) const; + +private: + template + [[nodiscard]] error_type filter_impl( + std::weak_ptr& logger, + std::vector& endpoints, + string_view url) const; +}; +} + +#endif //TORRENT_HTTP_TRACKER_REQUEST_HPP_INCLUDED diff --git a/include/libtorrent/aux_/intrusive_list.hpp b/include/libtorrent/aux_/intrusive_list.hpp new file mode 100644 index 00000000000..5f2660d4fee --- /dev/null +++ b/include/libtorrent/aux_/intrusive_list.hpp @@ -0,0 +1,244 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_INTRUSIVE_LIST_HPP_INCLUDED +#define TORRENT_INTRUSIVE_LIST_HPP_INCLUDED + +#include +#include +#include "libtorrent/assert.hpp" + +namespace libtorrent::aux { +// The `ownership_intrusive_list` class implements an intrusive list with smart_ptr ownership. +// +// Storage Overhead: +// - Requires 2 pointers per object +// +// Disadvantages: +// - Objects not in any `ownership_intrusive_list` still incur storage overhead of next/prev pointers. +// - Each object can belong to only one `ownership_intrusive_list` when using unique_ptr. + +template +struct unique_ptr_intrusive_list_base { + T* prev_ = nullptr; + std::unique_ptr next_; +}; + +// This type trait requires that T inherits from unique_ptr_intrusive_list_base +// note: additional type traits can be defined that can allow storing a list in a member variable or with a +// different type of smart_ptr +template +struct unique_ptr_intrusive_list_traits { + using node = T; + using node_ownership = std::unique_ptr; + + static_assert(std::is_base_of_v, T>); + + // for the move operations to be noexcept + static_assert(std::is_nothrow_move_constructible_v); + + static node_ownership take_next_ownership(node& n) noexcept + { + return std::move(n.next_); + } + + static node& set_next(node& n, node_ownership next) noexcept + { + TORRENT_ASSERT_PRECOND_MSG( + next, "set_next(n, nullptr) is a no-op"); + + TORRENT_ASSERT_PRECOND_MSG( + !n.next_, "Overwriting 'next' pointer risks accidentally deleting all list-items after this one recursively" + " and overflowing the stack"); + + n.next_ = std::move(next); + return *n.next_; + } + + static node* get_next(const node& n) noexcept { return n.next_.get(); } + static node* get_previous(const node& n) noexcept { return n.prev_; } + static void set_previous(node& n, node* prev) noexcept { n.prev_ = prev; } +}; + +template +class ownership_intrusive_list { + using node = typename type_trait::node; + using node_ownership = typename type_trait::node_ownership; + + // an exception in one of these functions would leave the list in an unspecified state + static_assert( + // must be able to transfer ownership + std::is_nothrow_move_constructible_v + // support pointer operations + && std::is_nothrow_invocable_v + && std::is_nothrow_invocable_v + // The value trait hides the next/prev pointer implementation from the list logic + // Make sure these functions exist on the value trait + && std::is_nothrow_invocable_v + && std::is_nothrow_invocable_v + && std::is_nothrow_invocable_v + && std::is_nothrow_invocable_v + && std::is_nothrow_invocable_v); + + node_ownership m_head; + + // optionally make this template dependant to optimize for space + std::size_t m_size = 0; + +public: + [[nodiscard]] std::size_t size() const noexcept + { + return m_size; + } + + void clear() noexcept + { + while (!empty()) + { + remove( *m_head ); + } + } + + ~ownership_intrusive_list() noexcept + { + clear(); + } + + [[nodiscard]] bool empty() const noexcept { return size() == 0; } + + // Add item to the front of the list + // Returns a reference to the newly added node, to signal that after transferring ownership, the object is still + // alive and can be accessed through this reference. Usually the address of the object does not change but that + // is an implementation detail of the move constructor. + node& add(node_ownership new_node) noexcept + { + TORRENT_ASSERT(new_node); + TORRENT_ASSERT_PRECOND_MSG(!type_trait::get_next(*new_node) && !type_trait::get_previous(*new_node), + "Attempt to add 'dirty' item that already belongs/belonged to a list"); + + if (empty()) + { + m_head = std::move(new_node); + } + else + { + auto& next = type_trait::set_next(*new_node, std::move(m_head)); + m_head = std::move(new_node); + type_trait::set_previous(next, m_head.get()); + } + + ++m_size; + return *m_head; + } + + node_ownership remove(std::reference_wrapper item) noexcept + { + TORRENT_ASSERT_PRECOND_MSG(m_head.get(), "cannot remove list-item from empty list"); + + node* next = type_trait::get_next(item); + node* prev = type_trait::get_previous(item); + + node_ownership item_ownership; + if (prev) + { + // remove item + type_trait::set_previous(item, nullptr); + item_ownership = type_trait::take_next_ownership(*prev); + + if (next) + { + // inner node, link neighbors + next = &type_trait::set_next(*prev, type_trait::take_next_ownership(*item_ownership)); + type_trait::set_previous(*next, prev); + } + } + else + { + // removing the head + item_ownership = std::move(m_head); + + if (next) + { + m_head = type_trait::take_next_ownership(*item_ownership); + type_trait::set_previous(*m_head, nullptr); + } + } + + --m_size; + return item_ownership; + } + + template + class basic_iterator { + using node_t = typename type_trait::node; + public: + using iterator_category = std::forward_iterator_tag; + using value_type = std::conditional_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type *; + using reference = value_type &; + + pointer m_node = nullptr; + friend class ownership_intrusive_list; + + explicit basic_iterator(pointer n) noexcept : m_node(n) + { + } + + basic_iterator() noexcept = default; + + // copy non-const -> const + template > + basic_iterator(const basic_iterator& other) noexcept : m_node(other.m_node) + { + } + + reference operator*() const noexcept { return *m_node; } + pointer operator->() const noexcept { return m_node; } + + basic_iterator& operator++() noexcept + { + m_node = type_trait::get_next(*m_node); + return *this; + } + + basic_iterator operator++(int) noexcept + { + basic_iterator tmp = *this; + ++*this; + return tmp; + } + + friend bool operator==(const basic_iterator& a, const basic_iterator& b) noexcept + { + return a.m_node == b.m_node; + } + + friend bool operator!=(const basic_iterator& a, const basic_iterator& b) noexcept + { + return a.m_node != b.m_node; + } + }; + + using iterator = basic_iterator; + using const_iterator = basic_iterator; + + [[nodiscard]] iterator begin() noexcept { return iterator{m_head.get()}; } + [[nodiscard]] constexpr iterator end() noexcept { return iterator{nullptr}; } + + [[nodiscard]] const_iterator begin() const noexcept { return const_iterator{m_head.get()}; } + [[nodiscard]] constexpr const_iterator end() const noexcept { return const_iterator{nullptr}; } +}; + +template +using unique_ptr_intrusive_list = ownership_intrusive_list>; +} + +#endif //TORRENT_INTRUSIVE_LIST_HPP_INCLUDED diff --git a/include/libtorrent/aux_/memory.hpp b/include/libtorrent/aux_/memory.hpp new file mode 100644 index 00000000000..73d2d94c1be --- /dev/null +++ b/include/libtorrent/aux_/memory.hpp @@ -0,0 +1,28 @@ +/* + +Copyright (c) 2026, Arvid Norberg +Copyright (c) 2026, The Baron Vladimir Harkonnen +All rights reserved. + +You may use, distribute and modify this code under the terms of the BSD license, +see LICENSE file. +*/ + +#ifndef TORRENT_MEMORY_HPP_INCLUDED +#define TORRENT_MEMORY_HPP_INCLUDED + +#include + +namespace libtorrent::aux { + +template +struct unique_ptr_destructor { + template + constexpr void operator()(T* arg) const { (void) F(arg); } +}; + +template +using unique_ptr_with_deleter = std::unique_ptr >; +} + +#endif //TORRENT_MEMORY_HPP_INCLUDED diff --git a/include/libtorrent/aux_/parse_url.hpp b/include/libtorrent/aux_/parse_url.hpp index 80b54cd2248..618fe2a1c0d 100644 --- a/include/libtorrent/aux_/parse_url.hpp +++ b/include/libtorrent/aux_/parse_url.hpp @@ -27,6 +27,23 @@ namespace libtorrent::aux { , std::string, int, std::string> parse_url_components(string_view url, error_code& ec); + class TORRENT_EXTRA_EXPORT exploded_url { + std::string m_protocol; + std::string m_auth; + std::string m_hostname; + std::string m_path; + int m_port; + public: + exploded_url(string_view url, error_code& ec); + exploded_url(exploded_url&&) = default; + + [[nodiscard]] string_view protocol() const { return m_protocol; } + [[nodiscard]] string_view auth() const { return m_auth; } + [[nodiscard]] string_view hostname() const { return m_hostname; } + [[nodiscard]] string_view path() const { return m_path; } + [[nodiscard]] int port() const { return m_port; } + }; + // split a URL in its base and path parts TORRENT_EXTRA_EXPORT std::tuple split_url(std::string url, error_code& ec); @@ -35,6 +52,8 @@ namespace libtorrent::aux { // name) labels. TORRENT_EXTRA_EXPORT bool is_idna(string_view hostname); + bool is_announce_path(string_view path); + // the query string is the part of the URL immediately following "?", i.e. // the query string arguments. This function returns true if any of the // arguments are "info_hash", "port", "key", "event", "uploaded", diff --git a/include/libtorrent/aux_/tracker_manager.hpp b/include/libtorrent/aux_/tracker_manager.hpp index 8b5135193c3..871c52a9d12 100644 --- a/include/libtorrent/aux_/tracker_manager.hpp +++ b/include/libtorrent/aux_/tracker_manager.hpp @@ -1,7 +1,7 @@ /* +Copyright (c) 2004-2026, Arvid Norberg Copyright (c) 2015, Mikhail Titov -Copyright (c) 2004-2022, Arvid Norberg Copyright (c) 2016, 2020-2021, Alden Torres Copyright (c) 2016-2017, Steven Siloti Copyright (c) 2020, Paul-Louis Ageneau @@ -51,6 +51,10 @@ see LICENSE file. #include "libtorrent/aux_/rtc_signaling.hpp" #endif +#if TORRENT_USE_CURL +#include "libtorrent/aux_/curl_tracker_manager.hpp" +#endif + namespace libtorrent { struct counters; @@ -275,7 +279,7 @@ using tracker_request_flags_t = flags::bitfield_flag p , error_code& ec, aux::udp_send_flags_t flags = {}); +#if !defined TORRENT_DISABLE_LOGGING || TORRENT_USE_ASSERTS + aux::session_logger& get_session() const noexcept { return m_ses; } +#endif private: // maps transactionid to the udp_tracker_connection @@ -400,6 +407,9 @@ using tracker_request_flags_t = flags::bitfield_flag +#include "libtorrent/aux_/parse_url.hpp" +#include "libtorrent/aux_/proxy_settings.hpp" +#include "libtorrent/string_view.hpp" + +using namespace libtorrent; + +namespace libtorrent::aux { + +curl_basic_request::curl_basic_request() + : m_curl_handle(curl_easy_init()) +{ +} + +errors::http_errors curl_basic_request::http_status() const +{ + // will be 0 if no response code is set. + auto code = getopt(); + return static_cast(code); +} + +address curl_basic_request::get_ip(error_code& ec) const +{ + const auto ip = getopt(); + return make_address(ip, ec); +} + +void curl_basic_request::set_defaults() +{ + // initialize a set of basic defaults, some default options are documented, and not explicitly set + + // Network config + // - CURLOPT_SSL_VERIFYSTATUS (default disabled, a.k.a. OSCP stapling/verification) + // - CURLOPT_SSL_ENABLE_ALPN (default enabled) + // - CURLOPT_CAPATH (unset, use system default) + // - CURLOPT_CAINFO (unset, use system default) + // - CURLOPT_ECH (default disabled, experimental, requires https-rr and is currently only supported with DoH resolver) + // - CURLOPT_HSTS_CTRL (default disabled, it is not port aware, and simply redirects all non-https traffic to + // port :443) + // - CURLOPT_TCP_KEEPALIVE (default disabled, keeping connections alive when idle is not supported by curl) + + // Setting SSL requirements protects against downgrading to insecure SSL configurations during a downgrade attack + // - CURLOPT_SSLVERSION (default = v1.2 or newer, note that this option is used to set a lower bound) + // - CURLOPT_SSL_CIPHER_LIST (use ssl library default, this is usually a subset of the "HIGH" encryption cipher list) + + // TLSv1.2+ became the default in 8.16.0 + static const bool has_insecure_minimum_ssl_version = curl_version_lower_than(0x081000); + + if (has_insecure_minimum_ssl_version) + setopt(CURL_SSLVERSION_TLSv1_2); // TLSv1.2 or newer + + // curl can pick up certain environment variables (HTTP_PROXY, NO_PROXY ...) which can make it diverge + // from the settings libtorrent uses. Setting PROXY to an empty string explicitly prevents this behavior. + setopt(""); + + // NOSIGNAL allows curl to request the OS not to use signals on the socket operations. The os will then use an + // error code instead of signals to communicate certain error conditions. It also stops curl from constantly + // toggling the SIGPIPE signal handler through expensive syscalls. Note that this means that SIGPIPE can + // *potentially* be received by libtorrent and it's users. Curl supports many platforms and that warning + // is not relevant to us. With NOSIGNAL the behavior should be similar to that of the boost library. + setopt(true); + + // CURLOPT_SHARE + // When using the curl multi interface these are always shared even without CURLOPT_SHARE: + // - CURL_LOCK_DATA_DNS + // - CURL_LOCK_DATA_SSL_SESSION + // - CURL_LOCK_DATA_PSL + // The following additional shares can be set: + // - CURL_LOCK_DATA_COOKIE (not used, trackers connections don't need them) + // - CURL_LOCK_DATA_HSTS (not used, by default disabled through CURLOPT_HSTS_CTRL) + + // Flow control + // - CURLOPT_CONNECTTIMEOUT (default: 300) + // - CURLOPT_DNS_CACHE_TIMEOUT (default: 60) + // - CURLOPT_FOLLOWLOCATION (default: off) + // - CURLOPT_MAXREDIRS (default: 30, depends on CURLOPT_FOLLOWLOCATION) + // - CURLOPT_AUTOREFERER (default: disabled) + + // CURLOPT_PIPEWAIT: Queue when a multiplexing connection is connecting/upgrading instead of creating a new connection. + // However, if the multiplexing connection is fully connected/upgraded but is unavailable due to reaching the maximum + // number of streams, a new connection is created instead of queuing. Default: false + + // prevent connecting/redirecting to a wrong scheme (e.g. file://evil/file") + setopt("http,https"); + + // HTTP headers + // empty string enables all built-in encodings + setopt(""); + +#if TORRENT_DEBUG_LIBCURL + set_debug_logging(true); +#endif +} + +bool curl_basic_request::bind(const std::string& device, const address& local_address) +{ + // note: curl binding network interfaces on device name is not supported on `windows`, binding on local IP works. + // No need to do anything special, device binding will quietly be ignored if not supported. + + // Users don't expect to leak DNS requests when explicitly binding a VPN interface. + // However, binding DNS is not implemented (same for the boost resolver). + // - If multiple nameservers are configured it is unclear which one to use and on which interface. + // - DNS nameservers might be hidden behind local DNS proxies (e.g. `127.0.0.1:53`, resolvectl). + // Adding settings for `nameservers` and `nameserver outgoing_interfaces` would be required. + + const bool valid_address = local_address.is_unspecified(); + if (device.empty() && !valid_address) + return false; + + // unset the ipv6 scope to remove it from to_string(), curl does not accept it + address addr = local_address.is_v4() ? local_address : make_address_v6(local_address.to_v6().to_bytes()); + const auto addr_str = addr.to_string(); + + std::string curl_interface; + if (!device.empty()) + { + if (!valid_address) + curl_interface = "if!" + device; + else + curl_interface = "ifhost!" + device + "!" + addr_str; + } + else + curl_interface = "host!" + addr_str; + + if (!curl_interface.empty()) + setopt(curl_interface); + + return true; +} + +void curl_basic_request::set_proxy(const proxy_settings& ps, const bool verify_ssl) +{ + if (ps.type == settings_pack::none) + return; + + if (!verify_ssl) + { + setopt(0L); + setopt(0L); + } + + if (ps.type == settings_pack::http_pw || ps.type == settings_pack::socks5_pw) + { + setopt(ps.username); + setopt(ps.password); + } + + setopt(ps.hostname); + setopt(static_cast(ps.port)); + + // Ignored for HTTP proxies: + // - send_host_in_connect: libcurl always sends the hostname in the "CONNECT: " for dns resolution, and in + // the "Host:" header + // - proxy_hostnames: the HTTP proxy is required to resolve the DNS by libcurl + + switch (ps.type) + { + case settings_pack::socks4: + setopt(ps.proxy_hostnames ? CURLPROXY_SOCKS4A : CURLPROXY_SOCKS4); + break; + case settings_pack::socks5: + case settings_pack::socks5_pw: + setopt(ps.proxy_hostnames ? CURLPROXY_SOCKS5_HOSTNAME : CURLPROXY_SOCKS5); + break; + case settings_pack::http: + case settings_pack::http_pw: + // Let libcurl set proxy type from hostname string without specifying it here + // CURLPROXY_HTTP (default) + // CURLPROXY_HTTP_1_0 (unused) + // CURLPROXY_HTTPS (when hostname starts with https://) + // CURLPROXY_HTTPS2 (unused) + break; +#if TORRENT_USE_I2P + case settings_pack::i2p_proxy: + TORRENT_ASSERT_FAIL(); + break; +#endif + case settings_pack::none: + break; + } +} + +namespace { +constexpr string_view curl_easy_option_str(CURLoption option) noexcept +{ + switch (option) + { + case CURLOPT_VERBOSE: return "CURLOPT_VERBOSE"; + case CURLOPT_DEBUGFUNCTION: return "CURLOPT_DEBUGFUNCTION"; + case CURLOPT_DEBUGDATA: return "CURLOPT_DEBUGDATA"; + case CURLOPT_WRITEFUNCTION: return "CURLOPT_WRITEFUNCTION"; + case CURLOPT_PIPEWAIT: return "CURLOPT_PIPEWAIT"; + case CURLOPT_USERPWD: return "CURLOPT_USERPWD"; + case CURLOPT_SSL_VERIFYPEER: return "CURLOPT_SSL_VERIFYPEER"; + case CURLOPT_SSL_VERIFYHOST: return "CURLOPT_SSL_VERIFYHOST"; + case CURLOPT_IPRESOLVE: return "CURLOPT_IPRESOLVE"; + case CURLOPT_PRIVATE: return "CURLOPT_PRIVATE"; + case CURLOPT_URL: return "CURLOPT_URL"; + case CURLOPT_USERAGENT: return "CURLOPT_USERAGENT"; + case CURLOPT_PREREQFUNCTION: return "CURLOPT_PREREQFUNCTION"; + case CURLOPT_PREREQDATA: return "CURLOPT_PREREQDATA"; + case CURLOPT_OPENSOCKETFUNCTION: return "CURLOPT_OPENSOCKETFUNCTION"; + case CURLOPT_OPENSOCKETDATA: return "CURLOPT_OPENSOCKETDATA"; + case CURLOPT_WRITEDATA: return "CURLOPT_WRITEDATA"; + case CURLOPT_ACCEPT_ENCODING: return "CURLOPT_ACCEPT_ENCODING"; + case CURLOPT_FOLLOWLOCATION: return "CURLOPT_FOLLOWLOCATION"; + case CURLOPT_NOSIGNAL: return "CURLOPT_NOSIGNAL"; + case CURLOPT_PROXY: return "CURLOPT_PROXY"; + case CURLOPT_DNS_INTERFACE: return "CURLOPT_DNS_INTERFACE"; + case CURLOPT_INTERFACE: return "CURLOPT_INTERFACE"; + case CURLOPT_DNS_LOCAL_IP4: return "CURLOPT_DNS_LOCAL_IP4"; + case CURLOPT_DNS_LOCAL_IP6: return "CURLOPT_DNS_LOCAL_IP6"; + case CURLOPT_PROXY_SSL_VERIFYHOST: return "CURLOPT_PROXY_SSL_VERIFYHOST"; + case CURLOPT_PROXY_SSL_VERIFYPEER: return "CURLOPT_PROXY_SSL_VERIFYPEER"; + case CURLOPT_PROXYUSERNAME: return "CURLOPT_PROXYUSERNAME"; + case CURLOPT_PROXYPASSWORD: return "CURLOPT_PROXYPASSWORD"; + case CURLOPT_PROXYPORT: return "CURLOPT_PROXYPORT"; + case CURLOPT_PROXYTYPE: return "CURLOPT_PROXYTYPE"; + case CURLOPT_SSLVERSION: return "CURLOPT_SSLVERSION"; + case CURLOPT_RESOLVER_START_FUNCTION: return "CURLOPT_RESOLVER_START_FUNCTION"; + case CURLOPT_RESOLVER_START_DATA: return "CURLOPT_RESOLVER_START_DATA"; + case CURLOPT_PROTOCOLS_STR: return "CURLOPT_PROTOCOLS_STR"; + default: + return ""; + } +} + +template +void throw_setop_ex(CURLoption option, CURLcode error, const T& value) +{ + if (error == CURLE_OUT_OF_MEMORY) + throw_ex(); + + std::string value_str; + bool value_set = false; + + if (error == CURLE_BAD_FUNCTION_ARGUMENT) + { + using basic_type = std::decay_t; + if constexpr (std::is_integral_v) + { + value_str = std::to_string(value); + value_set = true; + } + else if constexpr ( + std::is_same_v || + std::is_same_v || + std::is_same_v) + { + value_str = value; + value_set = true; + } + } + + if (value_set) + value_str = " to '" + value_str + "' "; + + const auto option_name = std::string(curl_easy_option_str(option)); + throw_ex(error, option_name + value_str); +} + +// Making `option` a compile time constant allows curl's typechecker +// to verify the types (it's currently not working/enabled for C++) +template +CURLcode curl_easy_setopt_wrapper(CURL* easy_handle, const T value) +{ + return curl_easy_setopt(easy_handle, option, value); +} + +template +CURLcode curl_easy_setopt_wrapper(CURL* easy_handle, const std::string& value) +{ + return curl_easy_setopt_wrapper