diff --git a/ChangeLog b/ChangeLog index 807cfc425..2ace30ac4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,4 @@ + * added UPnP support * fixed problem where peer interested flags were not updated correctly when pieces were filtered * improvements to ut_pex messages, including support for seed flag diff --git a/docs/features.html b/docs/features.html index c866c14dd..1e1dd486c 100644 --- a/docs/features.html +++ b/docs/features.html @@ -50,7 +50,7 @@ following features:

  • trackerless torrents (using the Mainline kademlia DHT protocol) with some DHT extensions.
  • support for IPv6
  • -
  • NAT-PMP support (automatic port mapping on routers that supports it)
  • +
  • NAT-PMP and UPnP support (automatic port mapping on routers that supports it)
  • piece-wise, unordered, incremental file allocation
  • uses separate threads for checking files and for main downloader, with a fool-proof thread-safe library interface. (i.e. There's no way for the diff --git a/docs/features.rst b/docs/features.rst index 04a21d6a9..2f4f83a32 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -32,7 +32,7 @@ following features: * trackerless torrents (using the Mainline kademlia DHT protocol) with some `DHT extensions`_. * support for IPv6 -* NAT-PMP support (automatic port mapping on routers that supports it) +* NAT-PMP and UPnP support (automatic port mapping on routers that supports it) * piece-wise, unordered, incremental file allocation * uses separate threads for checking files and for main downloader, with a fool-proof thread-safe library interface. (i.e. There's no way for the diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index 5f2f9b208..d1a4d9702 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -78,6 +78,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/file_pool.hpp" #include "libtorrent/bandwidth_manager.hpp" #include "libtorrent/natpmp.hpp" +#include "libtorrent/upnp.hpp" namespace libtorrent { @@ -393,6 +394,7 @@ namespace libtorrent int m_external_udp_port; #endif natpmp m_natpmp; + upnp m_upnp; // the timer used to fire the second_tick deadline_timer m_timer; diff --git a/include/libtorrent/upnp.hpp b/include/libtorrent/upnp.hpp index 05e963219..84e46646a 100644 --- a/include/libtorrent/upnp.hpp +++ b/include/libtorrent/upnp.hpp @@ -39,6 +39,8 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include #if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) #include @@ -57,6 +59,7 @@ class upnp : boost::noncopyable public: upnp(io_service& ios, address const& listen_interface , std::string const& user_agent, portmap_callback_t const& cb); + ~upnp(); void rebind(address const& listen_interface); @@ -67,6 +70,9 @@ public: void close(); private: + + enum { num_mappings = 2 }; + enum { default_lease_time = 3600 }; void update_mapping(int i, int port); void resend_request(asio::error_code const& e); @@ -81,32 +87,24 @@ private: void on_upnp_map_response(asio::error_code const& e , libtorrent::http_parser const& p, rootdevice& d , int mapping); - -/* - void send_map_request(int i); - void try_next_mapping(int i); - void update_expiration_timer(); - void refresh_mapping(int i); - void mapping_expired(asio::error_code const& e, int i); -*/ + void on_upnp_unmap_response(asio::error_code const& e + , libtorrent::http_parser const& p, rootdevice& d + , int mapping); + void on_expire(asio::error_code const& e); void post(rootdevice& d, std::stringstream const& s , std::string const& soap_action); void map_port(rootdevice& d, int i); - + void unmap_port(rootdevice& d, int i); + struct mapping_t { mapping_t() - : need_update(false) - , local_port(0) + : local_port(0) , external_port(0) , protocol(1) {} - // indicates that the mapping has changed - // and needs an update - bool need_update; - // the time the port mapping will expire boost::posix_time::ptime expires; @@ -125,8 +123,12 @@ private: struct rootdevice { - rootdevice(): ports_mapped(false), lease_duration(3600) - , supports_specific_external(true) {} + rootdevice(): lease_duration(default_lease_time) + , supports_specific_external(true) + { + mapping[0].protocol = 0; + mapping[1].protocol = 1; + } // the interface url, through which the list of // supported interfaces are fetched std::string url; @@ -136,10 +138,7 @@ private: // either the WANIP namespace or the WANPPP namespace std::string service_namespace; - mapping_t mapping[2]; - - // true if we already mapped the ports on this device - bool ports_mapped; + mapping_t mapping[num_mappings]; std::string hostname; int port; @@ -188,7 +187,12 @@ private: // timer used to refresh mappings deadline_timer m_refresh_timer; + // locks m_closing and m_devices + boost::mutex m_mutex; + boost::condition m_condvar; + bool m_disabled; + bool m_closing; #if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) std::ofstream m_log; diff --git a/src/session_impl.cpp b/src/session_impl.cpp index e4f10b30f..b975384a8 100755 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -496,6 +496,9 @@ namespace libtorrent { namespace detail #endif , m_natpmp(m_io_service, m_listen_interface.address() , bind(&session_impl::on_port_mapping, this, _1, _2, _3)) + , m_upnp(m_io_service, m_listen_interface.address() + , m_settings.user_agent + , bind(&session_impl::on_port_mapping, this, _1, _2, _3)) , m_timer(m_io_service) , m_checker_impl(*this) { @@ -687,6 +690,7 @@ namespace libtorrent { namespace detail } #endif m_natpmp.set_mappings(m_listen_interface.port(), 0); + m_upnp.set_mappings(m_listen_interface.port(), 0); if (m_listen_socket) async_accept(); } @@ -1078,6 +1082,7 @@ namespace libtorrent { namespace detail deadline_timer tracker_timer(m_io_service); // this will remove the port mappings m_natpmp.close(); + m_upnp.close(); session_impl::mutex_t::scoped_lock l(m_mutex); @@ -1471,8 +1476,12 @@ namespace libtorrent { namespace detail m_dht->rebind(new_interface.address() , m_dht_settings.service_port); if (new_listen_address) + { m_natpmp.rebind(new_interface.address()); + m_upnp.rebind(new_interface.address()); + } m_natpmp.set_mappings(0, m_dht_settings.service_port); + m_upnp.set_mappings(0, m_dht_settings.service_port); } #endif @@ -1595,6 +1604,7 @@ namespace libtorrent { namespace detail } m_external_udp_port = m_dht_settings.service_port; m_natpmp.set_mappings(0, m_dht_settings.service_port); + m_upnp.set_mappings(0, m_dht_settings.service_port); m_dht = new dht::dht_tracker(m_io_service , m_dht_settings, m_listen_interface.address() , startup_state); @@ -1625,6 +1635,7 @@ namespace libtorrent { namespace detail m_dht->rebind(m_listen_interface.address() , settings.service_port); m_natpmp.set_mappings(0, m_dht_settings.service_port); + m_upnp.set_mappings(0, m_dht_settings.service_port); m_external_udp_port = settings.service_port; } m_dht_settings = settings; diff --git a/src/upnp.cpp b/src/upnp.cpp index 8c9d69e1a..712d5213c 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -41,6 +41,7 @@ POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include using boost::bind; @@ -48,6 +49,8 @@ using namespace libtorrent; using boost::posix_time::microsec_clock; using boost::posix_time::milliseconds; using boost::posix_time::seconds; +using boost::posix_time::second_clock; +using boost::posix_time::ptime; enum { num_mappings = 2 }; @@ -64,6 +67,7 @@ upnp::upnp(io_service& ios, address const& listen_interface , m_broadcast_timer(ios) , m_refresh_timer(ios) , m_disabled(false) + , m_closing(false) { #if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) m_log.open("upnp.log", std::ios::in | std::ios::out | std::ios::trunc); @@ -71,6 +75,13 @@ upnp::upnp(io_service& ios, address const& listen_interface rebind(listen_interface); } +upnp::~upnp() +{ + boost::mutex::scoped_lock l(m_mutex); + while (!m_devices.empty()) + m_condvar.wait(l); +} + void upnp::rebind(address const& listen_interface) { if (listen_interface.is_v4() && listen_interface != address_v4::from_string("0.0.0.0")) @@ -269,7 +280,9 @@ void upnp::on_reply(asio::error_code const& e rootdevice d; d.url = url; - + + boost::mutex::scoped_lock l(m_mutex); + std::set::iterator i = m_devices.find(d); if (i == m_devices.end()) @@ -316,11 +329,6 @@ void upnp::on_reply(asio::error_code const& e , boost::bind(&upnp::on_upnp_xml, this, _1, _2, boost::ref(d)))); d.upnp_connection->get(d.url); } - else if (!i->ports_mapped) - { - // TODO: initiate a port map operation if any is pending - } - } void upnp::post(rootdevice& d, std::stringstream const& soap @@ -372,6 +380,33 @@ void upnp::map_port(rootdevice& d, int i) } +void upnp::unmap_port(rootdevice& d, int i) +{ + d.upnp_connection.reset(new http_connection(m_socket.io_service() + , boost::bind(&upnp::on_upnp_unmap_response, this, _1, _2 + , boost::ref(d), i))); + + std::string soap_action = "DeletePortMapping"; + + std::stringstream soap; + + soap << "\n" + "" + ""; + + soap << "" + "" << d.mapping[i].external_port << "" + "" << (d.mapping[i].protocol ? "UDP" : "TCP") << ""; + soap << ""; + + post(d, soap, soap_action); +#if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) + m_log << to_simple_string(microsec_clock::universal_time()) + << " ==> DeletePortMapping: " << soap.str() << std::endl; +#endif +} + namespace { struct parse_state @@ -507,6 +542,10 @@ void upnp::on_upnp_map_response(asio::error_code const& e return; } + boost::mutex::scoped_lock l(m_mutex); + if (m_closing) return; + l.unlock(); + // error code response may look like this: // @@ -568,9 +607,13 @@ void upnp::on_upnp_map_response(asio::error_code const& e m_callback(0, 0, "UPnP mapping error " + boost::lexical_cast(s.error_code) + ": " + error_codes[s.error_code]); } - - // TODO: update d.mapping[mapping].expires - std::cerr << std::string(p.get_body().begin, p.get_body().end) << std::endl; + +#if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) + m_log << to_simple_string(microsec_clock::universal_time()) + << " <== map response: " << std::string(p.get_body().begin, p.get_body().end) + << std::endl; +#endif + if (s.error_code == -1) { int tcp = 0; @@ -582,16 +625,111 @@ void upnp::on_upnp_map_response(asio::error_code const& e udp = d.mapping[mapping].external_port; m_callback(tcp, udp, ""); + if (d.lease_duration > 0) + { + d.mapping[mapping].expires = second_clock::universal_time() + seconds(d.lease_duration * 0.75); + ptime next_expire = m_refresh_timer.expires_at(); + if (next_expire == ptime(boost::date_time::not_a_date_time) + || next_expire < second_clock::universal_time() + || next_expire > d.mapping[mapping].expires) + { + m_refresh_timer.expires_at(d.mapping[mapping].expires); + m_refresh_timer.async_wait(bind(&upnp::on_expire, this, _1)); + } + } + else + { + d.mapping[mapping].expires = ptime(boost::date_time::not_a_date_time); + } } - if (mapping < 1) map_port(d, mapping + 1); + if (mapping < num_mappings - 1) + map_port(d, mapping + 1); +} + +void upnp::on_upnp_unmap_response(asio::error_code const& e + , libtorrent::http_parser const& p, rootdevice& d, int mapping) +{ + d.upnp_connection.reset(); + + if (e) + { +#if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) + m_log << to_simple_string(microsec_clock::universal_time()) + << " <== error while deleting portmap: " << e << std::endl; +#endif + } + +#if defined(TORRENT_LOGGING) || defined(TORRENT_VERBOSE_LOGGING) + m_log << to_simple_string(microsec_clock::universal_time()) + << " <== unmap response: " << std::string(p.get_body().begin, p.get_body().end) + << std::endl; +#endif + + // ignore errors and continue with the next mapping for this device + if (mapping < num_mappings - 1) + { + unmap_port(d, mapping + 1); + } + else + { + boost::mutex::scoped_lock l(m_mutex); + // the main thread is likely to be waiting for + // all the unmap operations to complete + m_devices.erase(d); + m_condvar.notify_all(); + } +} + +void upnp::on_expire(asio::error_code const& e) +{ + if (e) return; + + ptime now = second_clock::universal_time(); + ptime next_expire = ptime(boost::date_time::not_a_date_time); + + boost::mutex::scoped_lock l(m_mutex); + for (std::set::iterator i = m_devices.begin() + , end(m_devices.end()); i != end; ++i) + { + rootdevice& d = const_cast(*i); + for (int m = 0; m < num_mappings; ++m) + { + if (d.mapping[m].expires != ptime(boost::date_time::not_a_date_time)) + continue; + + if (d.mapping[m].expires < now) + { + d.mapping[m].expires = ptime(boost::date_time::not_a_date_time); + map_port(d, m); + } + else if (next_expire == ptime(boost::date_time::not_a_date_time) + || d.mapping[m].expires < next_expire) + { + next_expire = d.mapping[m].expires; + } + } + } + if (next_expire != ptime(boost::date_time::not_a_date_time)) + { + m_refresh_timer.expires_at(next_expire); + m_refresh_timer.async_wait(bind(&upnp::on_expire, this, _1)); + } } void upnp::close() { if (m_disabled) return; m_socket.close(); - std::for_each(m_devices.begin(), m_devices.end() - , bind(&rootdevice::close, _1)); + + boost::mutex::scoped_lock l(m_mutex); + m_closing = true; + + for (std::set::iterator i = m_devices.begin() + , end(m_devices.end()); i != end; ++i) + { + rootdevice& d = const_cast(*i); + unmap_port(d, 0); + } }