From 1ede34da8a5025cc2422869e8af0dbd31456e67d Mon Sep 17 00:00:00 2001 From: arvidn Date: Sun, 18 Feb 2018 10:36:57 +0100 Subject: [PATCH] undo the patch to restore incoming TCP connections over SOCKS5 --- ChangeLog | 1 - include/libtorrent/aux_/session_impl.hpp | 12 -- include/libtorrent/settings_pack.hpp | 6 - include/libtorrent/socks5_stream.hpp | 60 +-------- simulation/fake_peer.hpp | 7 - simulation/test_socks5.cpp | 157 +---------------------- simulation/test_transfer.cpp | 28 +--- src/session_impl.cpp | 131 ------------------- src/settings_pack.cpp | 1 - src/socks5_stream.cpp | 93 +------------- 10 files changed, 8 insertions(+), 488 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0267a5656..abb2dfb13 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,7 +5,6 @@ * fix torrent_status::next_announce * fix pad-file scalability issue * made coalesce_reads/coalesce_writes settings take effect on linux and windows - * restore support for incoming connections over SOCKS5 (disabled by default) * use unique peer_ids per connection * fix iOS build on recent SDK * fix tracker connection bind issue for IPv6 trackers diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index be8f7079d..3d60ce92f 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -239,10 +239,6 @@ namespace libtorrent void async_accept(boost::shared_ptr const& listener, bool ssl); void on_accept_connection(boost::shared_ptr const& s , boost::weak_ptr listener, error_code const& e, bool ssl); - void on_socks_listen(boost::shared_ptr const& s - , error_code const& e); - void on_socks_accept(boost::shared_ptr const& s - , error_code const& e); void incoming_connection(boost::shared_ptr const& s); @@ -653,7 +649,6 @@ namespace libtorrent void update_listen_interfaces(); void update_privileged_ports(); void update_auto_sequential(); - void update_incoming_socks5(); void update_max_failcount(); void update_close_file_interval(); @@ -891,16 +886,9 @@ namespace libtorrent void ssl_handshake(error_code const& ec, boost::shared_ptr s); #endif - // when as a socks proxy is used for peers, also - // listen for incoming connections on a socks connection - boost::shared_ptr m_socks_listen_socket; - boost::uint16_t m_socks_listen_port; - // round-robin index into m_net_interfaces mutable boost::uint8_t m_interface_index; - void open_new_incoming_socks_connection(); - enum listen_on_flags_t { open_ssl_socket = 0x10 diff --git a/include/libtorrent/settings_pack.hpp b/include/libtorrent/settings_pack.hpp index 0a08e3aa5..30657d2c9 100644 --- a/include/libtorrent/settings_pack.hpp +++ b/include/libtorrent/settings_pack.hpp @@ -696,12 +696,6 @@ namespace libtorrent // any. proxy_tracker_connections, - // this enables a SOCKS5 "extension", in which libtorrent attempts to - // receive incoming connections over a SOCKS5 proxy. The traditional - // interpretation of the RFC and normal SOCKS5 implementations do not - // support this. - incoming_socks5_connections, - max_bool_setting_internal }; diff --git a/include/libtorrent/socks5_stream.hpp b/include/libtorrent/socks5_stream.hpp index fb84f9603..e3979b350 100644 --- a/include/libtorrent/socks5_stream.hpp +++ b/include/libtorrent/socks5_stream.hpp @@ -90,7 +90,6 @@ public: // commands enum { socks5_connect = 1, - socks5_bind = 2, socks5_udp_associate = 3 }; @@ -98,7 +97,6 @@ public: : proxy_base(io_service) , m_version(5) , m_command(socks5_connect) - , m_listen(0) {} void set_version(int v) { m_version = v; } @@ -116,40 +114,6 @@ public: m_password = password; } - template - void async_accept(Handler const& handler) - { - TORRENT_ASSERT(m_listen == 1); - TORRENT_ASSERT(m_command == socks5_bind); - - // to avoid unnecessary copying of the handler, - // store it in a shaed_ptr - error_code e; -#if defined TORRENT_ASIO_DEBUGGING - add_outstanding_async("socks5_stream::connect1"); -#endif - connect1(e, boost::make_shared(handler)); - } - - template - void async_listen(tcp::endpoint const& ep, Handler const& handler) - { - m_command = socks5_bind; - - m_remote_endpoint = ep; - - // to avoid unnecessary copying of the handler, - // store it in a shaed_ptr - boost::shared_ptr h(new handler_type(handler)); - -#if defined TORRENT_ASIO_DEBUGGING - add_outstanding_async("socks5_stream::name_lookup"); -#endif - tcp::resolver::query q(m_hostname, to_string(m_port).elems); - m_resolver.async_resolve(q, boost::bind( - &socks5_stream::name_lookup, this, _1, _2, h)); - } - void set_dst_name(std::string const& host) { // if this assert trips, set_dst_name() is called wth an IP address rather @@ -175,25 +139,12 @@ public: } #endif -#ifndef BOOST_NO_EXCEPTIONS - endpoint_type local_endpoint() const - { - return m_local_endpoint; - } -#endif - - endpoint_type local_endpoint(error_code&) const - { - return m_local_endpoint; - } - // TODO: 2 add async_connect() that takes a hostname and port as well template void async_connect(endpoint_type const& endpoint, Handler const& handler) { // make sure we don't try to connect to INADDR_ANY. binding is fine, // and using a hostname is fine on SOCKS version 5. - TORRENT_ASSERT(m_command != socks5_bind); TORRENT_ASSERT(endpoint.address() != address() || (!m_dst_name.empty() && m_version == 5)); @@ -241,19 +192,10 @@ private: std::string m_password; std::string m_dst_name; - // when listening via a socks proxy, this is the IP and port our listen - // socket bound to - endpoint_type m_local_endpoint; - int m_version; - // the socks command to send for this connection (connect, bind, - // udp associate) + // the socks command to send for this connection (connect or udp associate) int m_command; - - // set to one when we're waiting for the - // second message to accept an incoming connection - int m_listen; }; } diff --git a/simulation/fake_peer.hpp b/simulation/fake_peer.hpp index 258862fdd..c417b8e61 100644 --- a/simulation/fake_peer.hpp +++ b/simulation/fake_peer.hpp @@ -130,13 +130,6 @@ private: { using namespace std::placeholders; - if (ec) - { - printf("fake_peer::connect: (%d) %s\n" - , ec.value(), ec.message().c_str()); - return; - } - asio::ip::tcp::endpoint const ep = m_out_socket.remote_endpoint(); printf("fake_peer::connect (%s) -> (%d) %s\n" , lt::print_endpoint(ep).c_str(), ec.value() diff --git a/simulation/test_socks5.cpp b/simulation/test_socks5.cpp index 80d90574e..7bbc87585 100644 --- a/simulation/test_socks5.cpp +++ b/simulation/test_socks5.cpp @@ -108,155 +108,6 @@ void run_test(Setup const& setup test(sim, *ses, params.ti); } -TORRENT_TEST(socks5_tcp_accept) -{ - using namespace libtorrent; - bool incoming_connection = false; - run_test( - [](lt::session& ses) - { - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses.apply_settings(p); - set_proxy(ses, settings_pack::socks5); - }, - [&](lt::session& ses, lt::alert const* alert) { - if (auto* a = lt::alert_cast(alert)) - { - TEST_EQUAL(a->socket_type, 2); - incoming_connection = true; - } - }, - [](sim::simulation& sim, lt::session& ses - , boost::shared_ptr ti) - { - // test connecting to the client via its socks5 listen port - // TODO: maybe we could use peer_conn here instead - fake_peer peer1(sim, "60.0.0.0"); - fake_peer peer2(sim, "60.0.0.1"); - - sim::timer t1(sim, lt::seconds(2), [&](boost::system::error_code const& ec) - { - peer1.connect_to(tcp::endpoint(addr("50.50.50.50"), 6881), ti->info_hash()); - }); - - sim::timer t2(sim, lt::seconds(3), [&](boost::system::error_code const& ec) - { - peer2.connect_to(tcp::endpoint(addr("50.50.50.50"), 6881), ti->info_hash()); - }); - - sim.run(); - } - ); - - TEST_EQUAL(incoming_connection, true); -} - -TORRENT_TEST(socks4_tcp_accept) -{ - using namespace libtorrent; - bool incoming_connection = false; - run_test( - [](lt::session& ses) - { - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses.apply_settings(p); - set_proxy(ses, settings_pack::socks4); - }, - [&](lt::session& ses, lt::alert const* alert) { - if (auto* a = lt::alert_cast(alert)) - { - TEST_EQUAL(a->socket_type, 2); - TEST_EQUAL(a->ip.address(), addr("60.0.0.0")) - incoming_connection = true; - } - }, - [](sim::simulation& sim, lt::session& ses - , boost::shared_ptr ti) - { - fake_peer peer1(sim, "60.0.0.0"); - - sim::timer t1(sim, lt::seconds(2), [&](boost::system::error_code const& ec) - { - peer1.connect_to(tcp::endpoint(addr("50.50.50.50"), 6881), ti->info_hash()); - }); - - sim.run(); - } - ); - - TEST_EQUAL(incoming_connection, true); -} - -// make sure a listen_succeeded_alert is issued when successfully listening on -// incoming connections via a socks5 proxy -TORRENT_TEST(socks4_tcp_listen_alert) -{ - using namespace libtorrent; - bool listen_alert = false; - run_test( - [](lt::session& ses) - { - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses.apply_settings(p); - set_proxy(ses, settings_pack::socks4); - }, - [&](lt::session& ses, lt::alert const* alert) { - if (auto* a = lt::alert_cast(alert)) - { - if (a->sock_type == listen_succeeded_alert::socks5) - { - TEST_EQUAL(a->endpoint.address(), addr("50.50.50.50")); - TEST_EQUAL(a->endpoint.port(), 6881); - listen_alert = true; - } - } - }, - [](sim::simulation& sim, lt::session& ses - , boost::shared_ptr ti) - { - sim.run(); - } - ); - - TEST_EQUAL(listen_alert, true); -} - -TORRENT_TEST(socks5_tcp_listen_alert) -{ - using namespace libtorrent; - bool listen_alert = false; - run_test( - [](lt::session& ses) - { - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses.apply_settings(p); - set_proxy(ses, settings_pack::socks5); - }, - [&](lt::session& ses, lt::alert const* alert) { - if (auto* a = lt::alert_cast(alert)) - { - if (a->sock_type == listen_succeeded_alert::socks5) - { - TEST_EQUAL(a->endpoint.address(), addr("50.50.50.50")); - TEST_EQUAL(a->endpoint.port(), 6881); - listen_alert = true; - } - } - }, - [](sim::simulation& sim, lt::session& ses - , boost::shared_ptr ti) - { - sim.run(); - } - ); - - TEST_EQUAL(listen_alert, true); -} - TORRENT_TEST(socks5_tcp_announce) { using namespace libtorrent; @@ -265,9 +116,6 @@ TORRENT_TEST(socks5_tcp_announce) run_test( [](lt::session& ses) { - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses.apply_settings(p); set_proxy(ses, settings_pack::socks5); lt::add_torrent_params params; @@ -279,7 +127,7 @@ TORRENT_TEST(socks5_tcp_announce) [&alert_port](lt::session& ses, lt::alert const* alert) { if (auto* a = lt::alert_cast(alert)) { - if (a->sock_type == listen_succeeded_alert::socks5) + if (a->sock_type == listen_succeeded_alert::udp) { alert_port = a->endpoint.port(); } @@ -313,7 +161,8 @@ TORRENT_TEST(socks5_tcp_announce) } ); - TEST_EQUAL(alert_port, tracker_port); + // since force_proxy is enabled, don't send the port + TEST_EQUAL(tracker_port, 0); TEST_CHECK(alert_port != -1); TEST_CHECK(tracker_port != -1); } diff --git a/simulation/test_transfer.cpp b/simulation/test_transfer.cpp index 2e4b4473d..566b777e6 100644 --- a/simulation/test_transfer.cpp +++ b/simulation/test_transfer.cpp @@ -51,8 +51,6 @@ using namespace sim; namespace lt = libtorrent; -const int connect_socks = 2; - template void run_test( Setup const& setup @@ -112,8 +110,7 @@ void run_test( print_alerts(*ses[0], [=](lt::session& ses, lt::alert const* a) { if (auto ta = alert_cast(a)) { - ta->handle.connect_peer(lt::tcp::endpoint( - (flags & connect_socks) ? proxy : peer1, 6881)); + ta->handle.connect_peer(lt::tcp::endpoint(peer1, 6881)); } on_alert(ses, a); }); @@ -179,29 +176,6 @@ TORRENT_TEST(socks5_tcp_connect) ); } -TORRENT_TEST(socks5_tcp_accept) -{ - using namespace libtorrent; - run_test( - [](lt::session& ses0, lt::session& ses1) - { - // this time, the session accepting the connection is listening on a - // socks5 BIND session - settings_pack p; - p.set_bool(settings_pack::incoming_socks5_connections, true); - ses1.apply_settings(p); - set_proxy(ses1, settings_pack::socks5); - filter_ips(ses0); - }, - [](lt::session& ses, lt::alert const* alert) {}, - [](std::shared_ptr ses[2]) { - TEST_EQUAL(is_seed(*ses[0]), true); - }, - connect_socks - ); -} - - TORRENT_TEST(encryption_tcp) { using namespace libtorrent; diff --git a/src/session_impl.cpp b/src/session_impl.cpp index cac6a61b4..d12df4698 100644 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -376,7 +376,6 @@ namespace aux { #if TORRENT_USE_I2P , m_i2p_conn(m_io_service) #endif - , m_socks_listen_port(0) , m_interface_index(0) , m_unchoke_time_scaler(0) , m_auto_manage_time_scaler(0) @@ -1097,12 +1096,6 @@ namespace aux { TORRENT_ASSERT(!ec); } m_listen_sockets.clear(); - if (m_socks_listen_socket && m_socks_listen_socket->is_open()) - { - m_socks_listen_socket->close(ec); - TORRENT_ASSERT(!ec); - } - m_socks_listen_socket.reset(); #if TORRENT_USE_I2P if (m_i2p_listen_socket && m_i2p_listen_socket->is_open()) @@ -2239,7 +2232,6 @@ retry: , end(m_listen_sockets.end()); i != end; ++i) async_accept(i->sock, i->ssl); - open_new_incoming_socks_connection(); #if TORRENT_USE_I2P open_new_incoming_i2p_connection(); #endif @@ -2287,98 +2279,6 @@ retry: } } - void session_impl::open_new_incoming_socks_connection() - { - int const proxy_type = m_settings.get_int(settings_pack::proxy_type); - - if (proxy_type != settings_pack::socks5 - && proxy_type != settings_pack::socks5_pw - && proxy_type != settings_pack::socks4) - return; - - if (!m_settings.get_bool(settings_pack::incoming_socks5_connections)) - return; - - if (m_socks_listen_socket) return; - - m_socks_listen_socket = boost::make_shared(boost::ref(m_io_service)); - bool const ret = instantiate_connection(m_io_service, proxy() - , *m_socks_listen_socket, NULL, NULL, false, false); - TORRENT_ASSERT_VAL(ret, ret); - TORRENT_UNUSED(ret); - -#if defined TORRENT_ASIO_DEBUGGING - add_outstanding_async("session_impl::on_socks_listen"); -#endif - socks5_stream& s = *m_socks_listen_socket->get(); - - m_socks_listen_port = m_listen_interface.port(); - if (m_socks_listen_port == 0) m_socks_listen_port = 2000 + random() % 60000; - s.async_listen(tcp::endpoint(address_v4::any(), m_socks_listen_port) - , boost::bind(&session_impl::on_socks_listen, this - , m_socks_listen_socket, _1)); - } - - void session_impl::on_socks_listen(boost::shared_ptr const& sock - , error_code const& e) - { -#if defined TORRENT_ASIO_DEBUGGING - complete_async("session_impl::on_socks_listen"); -#endif - - TORRENT_ASSERT(sock == m_socks_listen_socket || !m_socks_listen_socket); - - if (e) - { - m_socks_listen_socket.reset(); - if (e == boost::asio::error::operation_aborted) return; - if (m_alerts.should_post()) - m_alerts.emplace_alert("socks5" - , -1, listen_failed_alert::accept, e - , listen_failed_alert::socks5); - return; - } - - error_code ec; - tcp::endpoint ep = sock->local_endpoint(ec); - TORRENT_ASSERT(!ec); - TORRENT_UNUSED(ec); - - m_socks_listen_port = ep.port(); - - if (m_alerts.should_post()) - m_alerts.emplace_alert( - ep, listen_succeeded_alert::socks5); - -#if defined TORRENT_ASIO_DEBUGGING - add_outstanding_async("session_impl::on_socks_accept"); -#endif - socks5_stream& s = *m_socks_listen_socket->get(); - s.async_accept(boost::bind(&session_impl::on_socks_accept, this - , m_socks_listen_socket, _1)); - } - - void session_impl::on_socks_accept(boost::shared_ptr const& s - , error_code const& e) - { -#if defined TORRENT_ASIO_DEBUGGING - complete_async("session_impl::on_socks_accept"); -#endif - TORRENT_ASSERT(s == m_socks_listen_socket || !m_socks_listen_socket); - m_socks_listen_socket.reset(); - if (e == boost::asio::error::operation_aborted) return; - if (e) - { - if (m_alerts.should_post()) - m_alerts.emplace_alert("socks5" - , -1, listen_failed_alert::accept, e - , listen_failed_alert::socks5); - return; - } - open_new_incoming_socks_connection(); - incoming_connection(s); - } - void session_impl::update_i2p_bridge() { // we need this socket to be open before we @@ -5481,22 +5381,6 @@ retry: i->second->update_auto_sequential(); } - void session_impl::update_incoming_socks5() - { - if (!m_settings.get_bool(settings_pack::incoming_socks5_connections)) - { - if (m_socks_listen_socket) - { - m_socks_listen_socket->close(); - m_socks_listen_socket.reset(); - } - } - else - { - open_new_incoming_socks_connection(); - } - } - void session_impl::update_max_failcount() { for (torrent_map::iterator i = m_torrents.begin() @@ -5521,9 +5405,6 @@ retry: void session_impl::update_proxy() { - // in case we just set a socks proxy, we might have to - // open the socks incoming connection - if (!m_socks_listen_socket) open_new_incoming_socks_connection(); m_udp_socket.set_proxy_settings(proxy()); #ifdef TORRENT_USE_OPENSSL @@ -5608,12 +5489,6 @@ retry: boost::uint16_t session_impl::listen_port() const { - // if peer connections are set up to be received over a socks - // proxy, and it's the same one as we're using for the tracker - // just tell the tracker the socks5 port we're listening on - if (m_socks_listen_socket && m_socks_listen_socket->is_open()) - return m_socks_listen_port; - // if not, don't tell the tracker anything if we're in force_proxy // mode. We don't want to leak our listen port since it can // potentially identify us if it is leaked elsewere @@ -5625,12 +5500,6 @@ retry: boost::uint16_t session_impl::ssl_listen_port() const { #ifdef TORRENT_USE_OPENSSL - // if peer connections are set up to be received over a socks - // proxy, and it's the same one as we're using for the tracker - // just tell the tracker the socks5 port we're listening on - if (m_socks_listen_socket && m_socks_listen_socket->is_open()) - return m_socks_listen_port; - // if not, don't tell the tracker anything if we're in force_proxy // mode. We don't want to leak our listen port since it can // potentially identify us if it is leaked elsewere diff --git a/src/settings_pack.cpp b/src/settings_pack.cpp index f34b17919..e0a95b9e1 100644 --- a/src/settings_pack.cpp +++ b/src/settings_pack.cpp @@ -223,7 +223,6 @@ namespace libtorrent SET_NOPREV(proxy_peer_connections, true, 0), SET_NOPREV(auto_sequential, true, &session_impl::update_auto_sequential), SET_NOPREV(proxy_tracker_connections, true, 0), - SET_NOPREV(incoming_socks5_connections, false, &session_impl::update_incoming_socks5), }; int_setting_entry_t int_settings[settings_pack::num_int_settings] = diff --git a/src/socks5_stream.cpp b/src/socks5_stream.cpp index 71980cc97..eaf0a12c7 100644 --- a/src/socks5_stream.cpp +++ b/src/socks5_stream.cpp @@ -84,56 +84,6 @@ namespace libtorrent { return socks_category(); } #endif - namespace - { - // parse out the endpoint from a SOCKS response - tcp::endpoint parse_endpoint(std::vector const& buffer - , int const version) - { - using namespace libtorrent::detail; - char const* p = &buffer[0]; - p += 2; // version & response code - if (version == 5) - { - ++p; // reserved byte - int const atyp = read_uint8(p); - - if (atyp == 1) - { - tcp::endpoint ret; - ret.address(read_v4_address(p)); - ret.port(read_uint16(p)); - return ret; - } - else if (atyp == 3) - { - // we don't support resolving the endpoint address - // if we receive a domain name, just set the remote - // endpoint to INADDR_ANY - return tcp::endpoint(); - } - else if (atyp == 4) - { - tcp::endpoint ret; -#if TORRENT_USE_IPV6 - ret.address(read_v6_address(p)); - ret.port(read_uint16(p)); -#endif - return ret; - } - } - else if (version == 4) - { - tcp::endpoint ret; - ret.port(read_uint16(p)); - ret.address(read_v4_address(p)); - return ret; - } - TORRENT_ASSERT(false); - return tcp::endpoint(); - } - } - void socks5_stream::name_lookup(error_code const& e, tcp::resolver::iterator i , boost::shared_ptr h) { @@ -325,7 +275,7 @@ namespace libtorrent :(m_remote_endpoint.address().is_v4()?4:16))); char* p = &m_buffer[0]; write_uint8(5, p); // SOCKS VERSION 5 - write_uint8(m_command, p); // CONNECT/BIND command + write_uint8(m_command, p); // CONNECT command write_uint8(0, p); // reserved if (!m_dst_name.empty()) { @@ -338,8 +288,7 @@ namespace libtorrent else { // we either need a hostname or a valid endpoint - TORRENT_ASSERT(m_command == socks5_bind - || m_remote_endpoint.address() != address()); + TORRENT_ASSERT(m_remote_endpoint.address() != address()); write_uint8(m_remote_endpoint.address().is_v4()?1:4, p); // address type write_address(m_remote_endpoint.address(), p); @@ -357,7 +306,7 @@ namespace libtorrent m_buffer.resize(m_user.size() + 9); char* p = &m_buffer[0]; write_uint8(4, p); // SOCKS VERSION 4 - write_uint8(m_command, p); // CONNECT/BIND command + write_uint8(m_command, p); // CONNECT command write_uint16(m_remote_endpoint.port(), p); write_uint32(m_remote_endpoint.address().to_v4().to_ulong(), p); std::copy(m_user.begin(), m_user.end(), p); @@ -438,18 +387,6 @@ namespace libtorrent // on address type) if (atyp == 1) { - if (m_command == socks5_bind) - { - if (m_listen == 0) - { - m_local_endpoint = parse_endpoint(m_buffer, m_version); - m_listen = 1; - } - else - { - m_remote_endpoint = parse_endpoint(m_buffer, m_version); - } - } std::vector().swap(m_buffer); (*h)(e); return; @@ -490,18 +427,6 @@ namespace libtorrent // access granted if (response == 90) { - if (m_command == socks5_bind) - { - if (m_listen == 0) - { - m_local_endpoint = parse_endpoint(m_buffer, m_version); - m_listen = 1; - } - else - { - m_remote_endpoint = parse_endpoint(m_buffer, m_version); - } - } std::vector().swap(m_buffer); (*h)(e); return; @@ -527,18 +452,6 @@ namespace libtorrent if (handle_error(e, h)) return; - if (m_command == socks5_bind) - { - if (m_listen == 0) - { - m_local_endpoint = parse_endpoint(m_buffer, m_version); - m_listen = 1; - } - else - { - m_remote_endpoint = parse_endpoint(m_buffer, m_version); - } - } std::vector().swap(m_buffer); (*h)(e); }