/* Copyright (c) 2009, 2013-2021, Arvid Norberg Copyright (c) 2016-2017, 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_I2P_STREAM_HPP_INCLUDED #define TORRENT_I2P_STREAM_HPP_INCLUDED #include "libtorrent/config.hpp" #if TORRENT_USE_I2P #include #include #include #include #include "libtorrent/aux_/proxy_base.hpp" #include "libtorrent/aux_/string_util.hpp" #include "libtorrent/aux_/vector.hpp" #include "libtorrent/aux_/proxy_settings.hpp" #include "libtorrent/assert.hpp" #include "libtorrent/error_code.hpp" #include "libtorrent/settings_pack.hpp" #include "libtorrent/aux_/random.hpp" #include "libtorrent/hex.hpp" // for to_hex #include "libtorrent/aux_/debug.hpp" namespace libtorrent { namespace i2p_error { // error values for the i2p_category error_category. enum i2p_error_code { no_error = 0, parse_failed, cant_reach_peer, i2p_error, invalid_key, invalid_id, timeout, key_not_found, duplicated_id, num_errors }; // hidden TORRENT_EXPORT boost::system::error_code make_error_code(i2p_error_code e); } } namespace boost { namespace system { template<> struct is_error_code_enum { static const bool value = true; }; } } namespace libtorrent { // returns the error category for I2P errors TORRENT_EXPORT boost::system::error_category& i2p_category(); #if TORRENT_ABI_VERSION == 1 TORRENT_DEPRECATED inline boost::system::error_category& get_i2p_category() { return i2p_category(); } #endif struct i2p_session_options { int m_inbound_quantity = 3; int m_outbound_quantity = 3; int m_inbound_length = 3; int m_outbound_length = 3; int m_inbound_length_variance = 0; int m_outbound_length_variance = 0; }; struct i2p_stream : aux::proxy_base { explicit i2p_stream(io_context& io_context); i2p_stream(i2p_stream&&) noexcept = default; // explicitly disallow assignment, to silence msvc warning i2p_stream& operator=(i2p_stream const&) = delete; enum command_t : std::uint8_t { cmd_none, cmd_create_session, cmd_connect, cmd_accept, cmd_name_lookup, cmd_incoming }; void set_command(command_t c) { m_command = c; } void set_session_options(i2p_session_options const& session_options) { m_session_options = session_options; } void set_session_id(char const* id) { m_id = id; } void set_local_i2p_endpoint(string_view d) { m_local = d; } std::string const& local_i2p_endpoint() const { return m_local; } void set_destination(string_view d) { m_dest = d; } std::string const& destination() const { return m_dest; } template void async_connect(endpoint_type const&, Handler h) { // since we don't support regular endpoints, just ignore the one // provided and use m_dest. // the connect is split up in the following steps: // 1. resolve name of proxy server // 2. connect to SAM bridge // 4 send command message (CONNECT/ACCEPT) m_resolver.async_resolve(m_hostname, aux::to_string(m_port).data(), aux::wrap_allocator( [this](error_code const& ec, tcp::resolver::results_type ips, Handler hn) { do_connect(ec, std::move(ips), std::move(hn)); }, std::move(h))); } std::string name_lookup() const { return m_name_lookup; } void set_name_lookup(char const* name) { m_name_lookup = name; } template void send_name_lookup(Handler h) { TORRENT_ASSERT(m_magic == 0x1337); m_state = read_name_lookup_response; char cmd[1024]; int size = std::snprintf(cmd, sizeof(cmd), "NAMING LOOKUP NAME=%s\n", m_name_lookup.c_str()); ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line"); async_write(m_sock, boost::asio::buffer(cmd, std::size_t(size)), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { start_read_line(ec, std::move(hn)); }, std::move(h))); } private: template void do_connect(error_code const& e, tcp::resolver::results_type ips, Handler h) { TORRENT_ASSERT(m_magic == 0x1337); if (e || ips.empty()) { h(e); error_code ec; close(ec); return; } auto i = ips.begin(); ADD_OUTSTANDING_ASYNC("i2p_stream::connected"); m_sock.async_connect(i->endpoint(), aux::wrap_allocator( [this](error_code const& ec, Handler hn) { connected(ec, std::move(hn)); }, std::move(h))); } template void connected(error_code const& e, Handler h) { TORRENT_ASSERT(m_magic == 0x1337); COMPLETE_ASYNC("i2p_stream::connected"); if (handle_error(e, h)) return; // send hello command m_state = read_hello_response; static const char cmd[] = "HELLO VERSION MIN=3.1 MAX=3.1\n"; ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line"); async_write(m_sock, boost::asio::buffer(cmd, sizeof(cmd) - 1), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { start_read_line(ec, std::move(hn)); }, std::move(h))); } template void start_read_line(error_code const& e, Handler h) { TORRENT_ASSERT(m_magic == 0x1337); COMPLETE_ASYNC("i2p_stream::start_read_line"); if (handle_error(e, h)) return; ADD_OUTSTANDING_ASYNC("i2p_stream::read_line"); m_buffer.resize(1); async_read(m_sock, boost::asio::buffer(m_buffer), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { read_line(ec, std::move(hn)); }, std::move(h))); } template void read_line(error_code const& e, Handler h) { TORRENT_ASSERT(m_magic == 0x1337); COMPLETE_ASYNC("i2p_stream::read_line"); if (handle_error(e, h)) return; auto const read_pos = int(m_buffer.size()); // look for \n which means end of the response if (m_buffer[read_pos - 1] != '\n') { ADD_OUTSTANDING_ASYNC("i2p_stream::read_line"); // read another byte from the socket m_buffer.resize(read_pos + 1); async_read(m_sock, boost::asio::buffer(&m_buffer[read_pos], 1), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { read_line(ec, std::move(hn)); }, std::move(h))); return; } m_buffer[read_pos - 1] = 0; if (m_command == cmd_incoming) { // this is the line containing the destination // of the incoming connection in an accept call m_dest = &m_buffer[0]; h(e); std::vector().swap(m_buffer); return; } error_code invalid_response(i2p_error::parse_failed , i2p_category()); string_view expect1; string_view expect2; switch (m_state) { case read_hello_response: expect1 = "HELLO"_sv; expect2 = "REPLY"_sv; break; case read_connect_response: case read_accept_response: expect1 = "STREAM"_sv; expect2 = "STATUS"_sv; break; case read_session_create_response: expect1 = "SESSION"_sv; expect2 = "STATUS"_sv; break; case read_name_lookup_response: expect1 = "NAMING"_sv; expect2 = "REPLY"_sv; break; } TORRENT_ASSERT(m_buffer[int(m_buffer.size()) - 1] == '\0'); string_view remaining(m_buffer.data(), m_buffer.size() - 1); string_view token; std::tie(token, remaining) = aux::split_string(remaining, ' '); if (expect1.empty() || expect1 != token) { handle_error(invalid_response, h); return; } std::tie(token, remaining) = aux::split_string(remaining, ' '); if (expect2.empty() || expect2 != token) { handle_error(invalid_response, h); return; } int result = 0; for(;;) { string_view name; std::tie(name, remaining) = aux::split_string(remaining, '='); if (name.empty()) break; string_view value; if (!remaining.empty() && remaining[0] == '"') { std::tie(value, remaining) = aux::split_string(remaining.substr(1), '"'); } else { std::tie(value, remaining) = aux::split_string(remaining, ' '); } if ("RESULT"_sv == name) { if ("OK"_sv == value) result = i2p_error::no_error; else if ("CANT_REACH_PEER"_sv == value) result = i2p_error::cant_reach_peer; else if ("I2P_ERROR"_sv == value) result = i2p_error::i2p_error; else if ("INVALID_KEY"_sv == value) result = i2p_error::invalid_key; else if ("INVALID_ID"_sv == value) result = i2p_error::invalid_id; else if ("TIMEOUT"_sv == value) result = i2p_error::timeout; else if ("KEY_NOT_FOUND"_sv == value) result = i2p_error::key_not_found; else if ("DUPLICATED_ID"_sv == value) result = i2p_error::duplicated_id; else result = i2p_error::num_errors; // unknown error } /*else if ("MESSAGE" == name) { } else if ("VERSION"_sv == name) { }*/ else if ("VALUE"_sv == name) { m_name_lookup = value; } else if ("DESTINATION"_sv == name) { m_dest = value; } } error_code ec(result, i2p_category()); if (ec) { std::forward(h)(ec); return; } switch (m_state) { case read_hello_response: switch (m_command) { case cmd_create_session: send_session_create(std::move(h)); break; case cmd_accept: send_accept(std::move(h)); break; case cmd_connect: send_connect(std::move(h)); break; case cmd_none: case cmd_name_lookup: case cmd_incoming: std::forward(h)(ec); std::vector().swap(m_buffer); } break; case read_connect_response: case read_session_create_response: case read_name_lookup_response: std::forward(h)(ec); std::vector().swap(m_buffer); break; case read_accept_response: // the SAM bridge is waiting for an incoming // connection. // wait for one more line containing // the destination of the remote peer m_command = cmd_incoming; m_buffer.resize(1); ADD_OUTSTANDING_ASYNC("i2p_stream::read_line"); async_read(m_sock, boost::asio::buffer(m_buffer), aux::wrap_allocator( [this](error_code const& err, std::size_t, Handler hn) { read_line(err, std::move(hn)); }, std::move(h))); break; } } template void send_connect(Handler h) { TORRENT_ASSERT(m_magic == 0x1337); m_state = read_connect_response; char cmd[1024]; int size = std::snprintf(cmd, sizeof(cmd), "STREAM CONNECT ID=%s DESTINATION=%s\n" , m_id, m_dest.c_str()); ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line"); async_write(m_sock, boost::asio::buffer(cmd, std::size_t(size)), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { start_read_line(ec, std::move(hn)); }, std::move(h))); } template void send_accept(Handler h) { TORRENT_ASSERT(m_magic == 0x1337); m_state = read_accept_response; char cmd[400]; int size = std::snprintf(cmd, sizeof(cmd), "STREAM ACCEPT ID=%s\n", m_id); ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line"); async_write(m_sock, boost::asio::buffer(cmd, std::size_t(size)), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { start_read_line(ec, std::move(hn)); }, std::move(h))); } template void send_session_create(Handler h) { TORRENT_ASSERT(m_magic == 0x1337); m_state = read_session_create_response; char cmd[400]; int size = std::snprintf(cmd, sizeof(cmd), "SESSION CREATE STYLE=STREAM ID=%s " "DESTINATION=TRANSIENT SIGNATURE_TYPE=7 i2cp.leaseSetEncType=4,0 " "inbound.quantity=%d outbound.quantity=%d inbound.length=%d outbound.length=%d " "inbound.lengthVariance=%d outbound.lengthVariance=%d\n", m_id, m_session_options.m_inbound_quantity, m_session_options.m_outbound_quantity, m_session_options.m_inbound_length, m_session_options.m_outbound_length, m_session_options.m_inbound_length_variance, m_session_options.m_outbound_length_variance); ADD_OUTSTANDING_ASYNC("i2p_stream::start_read_line"); async_write(m_sock, boost::asio::buffer(cmd, std::size_t(size)), aux::wrap_allocator( [this](error_code const& ec, std::size_t, Handler hn) { start_read_line(ec, std::move(hn)); }, std::move(h))); } // send and receive buffer aux::vector m_buffer; char const* m_id = nullptr; std::string m_dest; std::string m_local; std::string m_name_lookup; i2p_session_options m_session_options; enum state_t : std::uint8_t { read_hello_response, read_connect_response, read_accept_response, read_session_create_response, read_name_lookup_response }; command_t m_command; state_t m_state; }; class TORRENT_EXTRA_EXPORT i2p_connection { public: explicit i2p_connection(io_context& ios); ~i2p_connection(); // explicitly disallow assignment, to silence msvc warning i2p_connection& operator=(i2p_connection const&) = delete; aux::proxy_settings proxy() const; bool is_open() const { return m_sam_socket && m_sam_socket->is_open() && m_state != sam_connecting; } template void open(std::string const& hostname, int port, i2p_session_options const& session_options, Handler handler) { // we already seem to have a session to this SAM router if (m_hostname == hostname && m_port == port && m_sam_socket && (is_open() || m_state == sam_connecting)) return; m_hostname = hostname; m_port = port; if (m_hostname.empty()) return; m_state = sam_connecting; char tmp[20]; aux::random_bytes(tmp); m_session_id.resize(sizeof(tmp)*2); aux::to_hex(tmp, &m_session_id[0]); m_sam_socket = std::make_shared(m_io_service); m_sam_socket->set_proxy(m_hostname, m_port); m_sam_socket->set_command(i2p_stream::cmd_create_session); m_sam_socket->set_session_id(m_session_id.c_str()); m_sam_socket->set_session_options(session_options); ADD_OUTSTANDING_ASYNC("i2p_stream::on_sam_connect"); m_sam_socket->async_connect(tcp::endpoint(), aux::wrap_allocator( [this,s=m_sam_socket](error_code const& ec, Handler hn) { on_sam_connect(ec, s, std::move(hn)); }, std::move(handler))); } void close(error_code&); // TODO: make this a string_view char const* session_id() const { return m_session_id.c_str(); } std::string const& local_endpoint() const { return m_i2p_local_endpoint; } template void async_name_lookup(char const* name, Handler handler) { if (m_state == sam_idle && m_name_lookup.empty() && is_open()) do_name_lookup(name, std::move(handler)); else m_name_lookup.emplace_back(std::string(name) , std::move(handler)); } private: template void on_sam_connect(error_code const& ec, std::shared_ptr, Handler h) { COMPLETE_ASYNC("i2p_stream::on_sam_connect"); m_state = sam_idle; if (ec) { h(ec); return; } do_name_lookup("ME", aux::wrap_allocator( [this](error_code const& e, char const* dst, Handler hn) { set_local_endpoint(e, dst, std::move(hn)); }, std::move(h))); } using name_lookup_handler = std::function; template void do_name_lookup(std::string const& name, Handler handler) { TORRENT_ASSERT(m_state == sam_idle); m_state = sam_name_lookup; m_sam_socket->set_name_lookup(name.c_str()); m_sam_socket->send_name_lookup(aux::wrap_allocator( [this,s=m_sam_socket](error_code const& ec, Handler hn) { on_name_lookup(ec, s, std::move(hn)); }, std::move(handler))); } template void on_name_lookup(error_code const& ec, std::shared_ptr, Handler handler) { m_state = sam_idle; std::string name = m_sam_socket->name_lookup(); if (!m_name_lookup.empty()) { std::pair& nl = m_name_lookup.front(); do_name_lookup(nl.first, std::move(nl.second)); m_name_lookup.pop_front(); } if (ec) { handler(ec, nullptr); return; } handler(ec, name.c_str()); } template void set_local_endpoint(error_code const& ec, char const* dest, Handler h) { if (!ec && dest != nullptr) m_i2p_local_endpoint = dest; else m_i2p_local_endpoint.clear(); h(ec); } // to talk to i2p SAM bridge std::shared_ptr m_sam_socket; std::string m_hostname; int m_port; // our i2p endpoint key std::string m_i2p_local_endpoint; std::string m_session_id; std::list> m_name_lookup; enum state_t { sam_connecting, sam_name_lookup, sam_idle }; state_t m_state; io_context& m_io_service; }; } #endif // TORRENT_USE_I2P #endif