/*

Copyright (c) 2019, Amir Abrams
Copyright (c) 2007-2022, Arvid Norberg
Copyright (c) 2009, Andrew Resch
Copyright (c) 2015, Mike Tzou
Copyright (c) 2016-2021, Alden Torres
Copyright (c) 2016-2017, Andrei Kurushin
Copyright (c) 2016-2017, Pavel Pimenov
Copyright (c) 2016, Steven Siloti
Copyright (c) 2020, Paul-Louis Ageneau
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include "libtorrent/config.hpp"
#include "libtorrent/socket.hpp"
#include "libtorrent/aux_/socket_io.hpp"
#include "libtorrent/upnp.hpp"
#include "libtorrent/aux_/io_bytes.hpp"
#include "libtorrent/aux_/parse_url.hpp"
#include "libtorrent/aux_/xml_parse.hpp"
#include "libtorrent/aux_/random.hpp"
#include "libtorrent/aux_/time.hpp" // for aux::time_now()
#include "libtorrent/aux_/http_connection.hpp"
#include "libtorrent/aux_/numeric_cast.hpp"
#include "libtorrent/aux_/ssl.hpp"
#include "libtorrent/aux_/scope_end.hpp"
#include "libtorrent/aux_/string_util.hpp" // for format_host_for_connect

#if defined TORRENT_ASIO_DEBUGGING
#include "libtorrent/aux_/debug.hpp"
#endif

#include "libtorrent/aux_/disable_warnings_push.hpp"
#include <boost/asio/ip/host_name.hpp>
#include <boost/asio/ip/multicast.hpp>
#include "libtorrent/aux_/disable_warnings_pop.hpp"

#include <cstdlib>
#include <cstdio> // for snprintf
#include <cstdarg>
#include <functional>

using namespace std::placeholders;

namespace libtorrent {

using namespace aux;

// due to the recursive nature of update_map, it's necessary to
// limit the internal list of global mappings to a small size
// this can be changed once the entire UPnP code is refactored
constexpr std::size_t max_global_mappings = 50;

namespace upnp_errors
{
	boost::system::error_code make_error_code(error_code_enum e)
	{ return {e, upnp_category()}; }

} // upnp_errors namespace

static error_code ignore_error;

bool aux::is_upnp_xml_content_type(string_view content_type)
{
	content_type = strip_string(split_string(content_type, ';').first);
	return string_equal_no_case(content_type, "text/xml"_sv)
		|| string_equal_no_case(content_type, "text/soap+xml"_sv)
		|| string_equal_no_case(content_type, "application/xml"_sv)
		|| string_equal_no_case(content_type, "application/soap+xml"_sv);
}

upnp::rootdevice::rootdevice() = default;

#if TORRENT_USE_ASSERTS
upnp::rootdevice::~rootdevice()
{
	TORRENT_ASSERT(magic == 1337);
	magic = 0;
}
#else
upnp::rootdevice::~rootdevice() = default;
#endif

upnp::rootdevice::rootdevice(rootdevice const&) = default;
upnp::rootdevice& upnp::rootdevice::operator=(rootdevice const&) & = default;
upnp::rootdevice::rootdevice(rootdevice&&) noexcept = default;
upnp::rootdevice& upnp::rootdevice::operator=(rootdevice&&) & = default;

// TODO: 2 use boost::asio::ip::network instead of netmask
upnp::upnp(io_context& ios
	, aux::session_settings const& settings
	, aux::portmap_callback& cb
	, address_v4 const listen_address
	, address_v4 const netmask
	, std::string listen_device
	, listen_socket_handle ls)
	: m_settings(settings)
	, m_callback(cb)
	, m_io_service(ios)
	, m_resolver(ios)
	, m_multicast(ios)
	, m_unicast(ios)
	, m_broadcast_timer(ios)
	, m_refresh_timer(ios)
	, m_map_timer(ios)
	, m_listen_address(listen_address)
	, m_netmask(netmask)
	, m_device(std::move(listen_device))
#if TORRENT_USE_SSL
	, m_ssl_ctx(ssl::context::sslv23_client)
#endif
	, m_listen_handle(std::move(ls))
{
#if TORRENT_USE_SSL
	m_ssl_ctx.set_verify_mode(aux::ssl::context::verify_none);
#endif
}

void upnp::start()
{
	TORRENT_ASSERT(is_single_thread());

	error_code ec;
	open_multicast_socket(m_multicast, ec);
#ifndef TORRENT_DISABLE_LOGGING
	if (ec && should_log())
	{
		log("failed to open multicast socket: \"%s\""
			, ec.message().c_str());
		m_disabled = true;
		return;
	}
#endif

	open_unicast_socket(m_unicast, ec);
#ifndef TORRENT_DISABLE_LOGGING
	if (ec && should_log())
	{
		log("failed to open unicast socket: \"%s\""
			, ec.message().c_str());
		m_disabled = true;
		return;
	}
#endif

	m_mappings.reserve(2);

	discover_device_impl();
}

namespace {
	address_v4 const ssdp_multicast_addr = make_address_v4("239.255.255.250");
	int const ssdp_port = 1900;

}

void upnp::open_multicast_socket(aux::socket_package& s, error_code& ec)
{
	using namespace boost::asio::ip::multicast;
	s.socket.open(udp::v4(), ec);
	if (ec) return;
	s.socket.set_option(udp::socket::reuse_address(true), ec);
	if (ec) return;
	s.socket.bind(udp::endpoint(m_listen_address, ssdp_port), ec);
	if (ec) return;
	s.socket.non_blocking(true, ec);
	if (ec) return;
	s.socket.set_option(join_group(ssdp_multicast_addr), ec);
	if (ec) return;
	s.socket.set_option(hops(255), ec);
	if (ec) return;
	s.socket.set_option(enable_loopback(true), ec);
	if (ec) return;
	s.socket.set_option(outbound_interface(m_listen_address), ec);
	if (ec) return;

	ADD_OUTSTANDING_ASYNC("upnp::on_reply");
	s.socket.async_receive_from(boost::asio::buffer(s.buffer), s.remote
		, std::bind(&upnp::on_reply, self(), std::ref(s), _1, _2));
}

void upnp::open_unicast_socket(aux::socket_package& s, error_code& ec)
{
	s.socket.open(udp::v4(), ec);
	if (ec) return;
	s.socket.bind(udp::endpoint(m_listen_address, 0), ec);
	if (ec) return;
	s.socket.non_blocking(true, ec);
	if (ec) return;

	ADD_OUTSTANDING_ASYNC("upnp::on_reply");
	s.socket.async_receive_from(boost::asio::buffer(s.buffer), s.remote
		, std::bind(&upnp::on_reply, self(), std::ref(s), _1, _2));
}

upnp::~upnp() = default;

#ifndef TORRENT_DISABLE_LOGGING
bool upnp::should_log() const
{
	return m_callback.should_log_portmap(portmap_transport::upnp);
}

TORRENT_FORMAT(2,3)
void upnp::log(char const* fmt, ...) const
{
	TORRENT_ASSERT(is_single_thread());
	if (!should_log()) return;
	va_list v;
	va_start(v, fmt);
	char msg[1024];
	std::vsnprintf(msg, sizeof(msg), fmt, v);
	va_end(v);
	m_callback.log_portmap(portmap_transport::upnp, msg, m_listen_handle);
}
#endif

void upnp::discover_device_impl()
{
	TORRENT_ASSERT(is_single_thread());
	static const char msearch[] =
		"M-SEARCH * HTTP/1.1\r\n"
		"HOST: 239.255.255.250:1900\r\n"
		"ST:upnp:rootdevice\r\n"
		"MAN:\"ssdp:discover\"\r\n"
		"MX:3\r\n"
		"\r\n\r\n";

#ifdef TORRENT_DEBUG_UPNP
	// simulate packet loss
	if (m_retry_count & 1)
#endif

	error_code mcast_ec;
	error_code ucast_ec;
	m_multicast.socket.send_to(boost::asio::buffer(msearch, sizeof(msearch) - 1)
		, udp::endpoint(ssdp_multicast_addr, ssdp_port), 0, mcast_ec);
	m_unicast.socket.send_to(boost::asio::buffer(msearch, sizeof(msearch) - 1)
		, udp::endpoint(ssdp_multicast_addr, ssdp_port), 0, ucast_ec);

	auto const dropped = [](error_code const& e) {
		return e == boost::asio::error::would_block || e == boost::asio::error::try_again;
	};

	if (mcast_ec && ucast_ec && !dropped(mcast_ec) && !dropped(ucast_ec))
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("multicast send failed: \"%s\" and \"%s\". Aborting."
				, mcast_ec.message().c_str()
				, ucast_ec.message().c_str());
		}
#endif
		disable(mcast_ec);
		return;
	}

	ADD_OUTSTANDING_ASYNC("upnp::resend_request");
	++m_retry_count;
	m_broadcast_timer.expires_after(seconds(2 * m_retry_count));
	m_broadcast_timer.async_wait(std::bind(&upnp::resend_request
		, self(), _1));

#ifndef TORRENT_DISABLE_LOGGING
	log("broadcasting search for rootdevice");
#endif
}

// returns a reference to a mapping or -1 on failure
port_mapping_t upnp::add_mapping(portmap_protocol const p, int const external_port
	, tcp::endpoint const local_ep, std::string const& device)
{
	TORRENT_ASSERT(is_single_thread());
	// external port 0 means _every_ port
	TORRENT_ASSERT(external_port != 0);

#ifndef TORRENT_DISABLE_LOGGING
	if (should_log())
	{
		log("adding port map: [ protocol: %s ext_port: %d "
			"local_ep: %s device: %s] %s", (p == portmap_protocol::tcp?"tcp":"udp")
			, external_port
			, print_endpoint(local_ep).c_str(), device.c_str()
			, m_disabled ? "DISABLED": "");
	}
#endif
	if (m_disabled) return port_mapping_t{-1};

	auto mapping_it = std::find_if(m_mappings.begin(), m_mappings.end()
		, [](global_mapping_t const& m) { return m.protocol == portmap_protocol::none; });

	if (mapping_it == m_mappings.end())
	{
		TORRENT_ASSERT(m_mappings.size() <= max_global_mappings);
		if (m_mappings.size() >= max_global_mappings)
		{
#ifndef TORRENT_DISABLE_LOGGING
			log("too many mappings registered");
#endif
			return port_mapping_t{-1};
		}
		m_mappings.push_back(global_mapping_t());
		mapping_it = m_mappings.end() - 1;
	}

	mapping_it->protocol = p;
	mapping_it->external_port = external_port;
	mapping_it->local_ep = local_ep;
	mapping_it->device = device;

	port_mapping_t const mapping_index{static_cast<int>(mapping_it - m_mappings.begin())};

	for (auto const& dev : m_devices)
	{
		auto& d = const_cast<rootdevice&>(dev);
		TORRENT_ASSERT(d.magic == 1337);
		if (d.disabled) continue;

		if (d.mapping.end_index() <= mapping_index)
			d.mapping.resize(static_cast<int>(mapping_index) + 1);
		mapping_t& m = d.mapping[mapping_index];

		m.act = portmap_action::add;
		m.protocol = p;
		m.external_port = external_port;
		m.local_ep = local_ep;
		m.device = device;

		if (!d.service_namespace.empty()) update_map(d, mapping_index);
	}

	return port_mapping_t{mapping_index};
}

void upnp::delete_mapping(port_mapping_t const mapping)
{
	TORRENT_ASSERT(is_single_thread());

	if (mapping >= m_mappings.end_index()) return;

	global_mapping_t const& m = m_mappings[mapping];

#ifndef TORRENT_DISABLE_LOGGING
	if (should_log())
	{
		log("deleting port map: [ protocol: %s ext_port: %d "
			"local_ep: %s device: %s]"
			, (m.protocol == portmap_protocol::tcp?"tcp":"udp"), m.external_port
			, print_endpoint(m.local_ep).c_str()
			, m.device.c_str());
	}
#endif

	if (m.protocol == portmap_protocol::none) return;

	for (auto const& dev : m_devices)
	{
		auto& d = const_cast<rootdevice&>(dev);
		TORRENT_ASSERT(d.magic == 1337);
		if (d.disabled) continue;

		TORRENT_ASSERT(mapping < d.mapping.end_index());
		d.mapping[mapping].act = portmap_action::del;

		if (!d.service_namespace.empty()) update_map(d, mapping);
	}
}

bool upnp::get_mapping(port_mapping_t const index
	, tcp::endpoint& local_ep
	, int& external_port
	, portmap_protocol& protocol) const
{
	TORRENT_ASSERT(is_single_thread());
	TORRENT_ASSERT(index < m_mappings.end_index() && index >= port_mapping_t{0});
	if (index >= m_mappings.end_index() || index < port_mapping_t{0}) return false;
	global_mapping_t const& m = m_mappings[index];
	if (m.protocol == portmap_protocol::none) return false;
	local_ep = m.local_ep;
	external_port = m.external_port;
	protocol = m.protocol;
	return true;
}

void upnp::resend_request(error_code const& ec)
{
	TORRENT_ASSERT(is_single_thread());
	COMPLETE_ASYNC("upnp::resend_request");
	if (ec) return;

	std::shared_ptr<upnp> me(self());

	if (m_closing) return;

	if (m_retry_count < 12
		&& (m_devices.empty() || m_retry_count < 4))
	{
		discover_device_impl();
		return;
	}

	if (m_devices.empty())
	{
		disable(errors::no_router);
		return;
	}

	for (auto const& dev : m_devices)
	{
		if (!dev.control_url.empty()
			|| dev.upnp_connection
			|| dev.disabled)
		{
			continue;
		}

		// we don't have a WANIP or WANPPP url for this device,
		// ask for it
		connect(const_cast<rootdevice&>(dev));
	}
}

void upnp::connect(rootdevice& d)
{
	TORRENT_ASSERT(d.magic == 1337);
	try
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("connecting to: %s", d.url.c_str());
#endif
		if (d.upnp_connection) d.upnp_connection->close();
		d.upnp_connection = std::make_shared<aux::http_connection>(m_io_service
			, m_resolver
			, std::bind(&upnp::on_upnp_xml, self(), _1, _2
				, std::ref(d), _4), default_max_bottled_buffer_size
			, http_connect_handler()
			, http_filter_handler()
			, hostname_filter_handler()
#if TORRENT_USE_SSL
			, &m_ssl_ctx
#endif
			);
		d.upnp_connection->get(d.url, seconds(30));
	}
	catch (std::exception const& exc)
	{
		TORRENT_UNUSED(exc);
#ifndef TORRENT_DISABLE_LOGGING
		log("connection failed to: %s %s", d.url.c_str(), exc.what());
#endif
		d.disabled = true;
	}
}

void upnp::on_reply(aux::socket_package& s, error_code const& ec, std::size_t const len)
{
	TORRENT_ASSERT(is_single_thread());
	COMPLETE_ASYNC("upnp::on_reply");

	if (ec == boost::asio::error::operation_aborted) return;
	if (m_closing) return;

	std::shared_ptr<upnp> me(self());
	udp::endpoint const from = s.remote;
	// parse out the url for the device

/*
	the response looks like this:

	HTTP/1.1 200 OK
	ST:upnp:rootdevice
	USN:uuid:000f-66d6-7296000099dc::upnp:rootdevice
	Location: http://192.168.1.1:5431/dyndev/uuid:000f-66d6-7296000099dc
	Server: Custom/1.0 UPnP/1.0 Proc/Ver
	EXT:
	Cache-Control:max-age=180
	DATE: Fri, 02 Jan 1970 08:10:38 GMT

	a notification looks like this:

	NOTIFY * HTTP/1.1
	Host:239.255.255.250:1900
	NT:urn:schemas-upnp-org:device:MediaServer:1
	NTS:ssdp:alive
	Location:http://10.0.3.169:2869/upnphost/udhisapi.dll?content=uuid:c17f0c32-d19b-4938-ae94-65f945c3a26e
	USN:uuid:c17f0c32-d19b-4938-ae94-65f945c3a26e::urn:schemas-upnp-org:device:MediaServer:1
	Cache-Control:max-age=900
	Server:Microsoft-Windows-NT/5.1 UPnP/1.0 UPnP-Device-Host/1.0

*/

	// reissue the async receive as we exit the function. We can't do this
	// earlier because we still need to use s.buffer
	auto reissue_receive = scope_end([&] {
		ADD_OUTSTANDING_ASYNC("upnp::on_reply");
		s.socket.async_receive_from(boost::asio::buffer(s.buffer), s.remote
			, std::bind(&upnp::on_reply, self(), std::ref(s), _1, _2));
	});

	if (ec) return;

	if (m_settings.get_bool(settings_pack::upnp_ignore_nonrouters)
		&& !match_addr_mask(m_listen_address, from.address(), m_netmask))
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("ignoring response from: %s. IP is not on local network. (addr: %s mask: %s)"
				, print_endpoint(from).c_str()
				, m_listen_address.to_string().c_str()
				, m_netmask.to_string().c_str());
		}
#endif
		return;
	}

	aux::http_parser p;
	bool error = false;
	p.incoming({s.buffer.data(), std::ptrdiff_t(len)}, error);
	if (error)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("received malformed HTTP from: %s"
				, print_endpoint(from).c_str());
		}
#endif
		return;
	}

	if (p.status_code() != 200 && p.method() != "notify")
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			if (p.method().empty())
			{
				log("HTTP status %d from %s"
					, p.status_code(), print_endpoint(from).c_str());
			}
			else
			{
				log("HTTP method %s from %s"
					, p.method().c_str(), print_endpoint(from).c_str());
			}
		}
#endif
		return;
	}

	if (!p.header_finished())
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("incomplete HTTP packet from %s"
				, print_endpoint(from).c_str());
		}
#endif
		return;
	}

	std::string url = p.header("location");
	if (url.empty())
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("missing location header from %s"
				, print_endpoint(from).c_str());
		}
#endif
		return;
	}

	rootdevice d;
	d.url = url;

	auto i = m_devices.find(d);

	if (i == m_devices.end())
	{
		std::string protocol;
		std::string auth;
		error_code err;
		// we don't have this device in our list. Add it
		std::tie(protocol, auth, d.hostname, d.port, d.path)
			= parse_url_components(d.url, err);
		if (d.port == -1) d.port = protocol == "http" ? 80 : 443;

		if (err)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				log("invalid URL %s from %s: %s"
					, d.url.c_str(), print_endpoint(from).c_str(), err.message().c_str());
			}
#endif
			return;
		}

		// ignore the auth here. It will be re-parsed
		// by the http connection later

		if (protocol != "http")
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				log("unsupported protocol %s from %s"
					, protocol.c_str(), print_endpoint(from).c_str());
			}
#endif
			return;
		}

		if (d.port == 0)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				log("URL with port 0 from %s", print_endpoint(from).c_str());
			}
#endif
			return;
		}

#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("found rootdevice: %s (%d)"
				, d.url.c_str(), int(m_devices.size()));
		}
#endif

		if (m_devices.size() >= 50)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (should_log())
			{
				log("too many rootdevices: (%d). Ignoring %s"
					, int(m_devices.size()), d.url.c_str());
			}
#endif
			return;
		}

		TORRENT_ASSERT(d.mapping.empty());
		for (auto const& j : m_mappings)
		{
			mapping_t m;
			m.act = portmap_action::add;
			m.local_ep = j.local_ep;
			m.device = j.device;
			m.external_port = j.external_port;
			m.protocol = j.protocol;
			d.mapping.push_back(m);
		}
		std::tie(i, std::ignore) = m_devices.insert(d);
	}


	// iterate over the devices we know and connect and issue the mappings
	try_map_upnp();

	// check back in a little bit to see if we have seen any
	// devices at one of our default routes. If not, we want to override
	// ignoring them and use them instead (better than not working).
	m_map_timer.expires_after(seconds(1));
	ADD_OUTSTANDING_ASYNC("upnp::map_timer");
	m_map_timer.async_wait(std::bind(&upnp::map_timer, self(), _1));
}

void upnp::map_timer(error_code const& ec)
{
	TORRENT_ASSERT(is_single_thread());
	COMPLETE_ASYNC("upnp::map_timer");
	if (ec) return;
	if (m_closing) return;

	try_map_upnp();
}

void upnp::try_map_upnp()
{
	TORRENT_ASSERT(is_single_thread());
	if (m_devices.empty()) return;

	for (auto i = m_devices.begin(), end(m_devices.end()); i != end; ++i)
	{
		if (i->control_url.empty() && !i->upnp_connection && !i->disabled)
		{
			// we don't have a WANIP or WANPPP url for this device,
			// ask for it
			connect(const_cast<rootdevice&>(*i));
		}
	}
}

void upnp::post(upnp::rootdevice const& d, char const* soap
	, char const* soap_action)
{
	TORRENT_ASSERT(is_single_thread());
	TORRENT_ASSERT(d.magic == 1337);
	TORRENT_ASSERT(d.upnp_connection);
	TORRENT_ASSERT(!d.disabled);

	// IPv6 literals must be bracketed in the Host header (RFC 3986); parse_url
	// strips the brackets, so re-add them here for v6 device addresses.
	std::string const host =
		format_host_for_connect(d.hostname, static_cast<unsigned short>(d.port));
	char header[2048];
	std::snprintf(header,
		sizeof(header),
		"POST %s HTTP/1.1\r\n"
		"Host: %s\r\n"
		"Content-Type: text/xml; charset=\"utf-8\"\r\n"
		"Content-Length: %d\r\n"
		"Soapaction: \"%s#%s\"\r\n\r\n"
		"%s",
		d.path.c_str(),
		host.c_str(),
		int(strlen(soap)),
		d.service_namespace.c_str(),
		soap_action,
		soap);

	d.upnp_connection->m_sendbuffer = header;

#ifndef TORRENT_DISABLE_LOGGING
	log("sending: %s", header);
#endif
}

int upnp::lease_duration(rootdevice const& d) const
{
	return d.use_lease_duration
		? m_settings.get_int(settings_pack::upnp_lease_duration)
		: 0;
}

void upnp::create_port_mapping(aux::http_connection& c, rootdevice& d
	, port_mapping_t const i)
{
	TORRENT_ASSERT(is_single_thread());

	TORRENT_ASSERT(d.magic == 1337);

	if (!d.upnp_connection)
	{
		TORRENT_ASSERT(d.disabled);
#ifndef TORRENT_DISABLE_LOGGING
		log("mapping %d aborted", static_cast<int>(i));
#endif
		return;
	}

	char const* soap_action = "AddPortMapping";

	error_code ec;
	std::string local_endpoint = print_address(c.socket().local_endpoint(ec).address());

	char soap[1024];
	std::snprintf(soap, sizeof(soap), "<?xml version=\"1.0\"?>\n"
		"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
		"<s:Body><u:%s xmlns:u=\"%s\">"
		"<NewRemoteHost></NewRemoteHost>"
		"<NewExternalPort>%d</NewExternalPort>"
		"<NewProtocol>%s</NewProtocol>"
		"<NewInternalPort>%d</NewInternalPort>"
		"<NewInternalClient>%s</NewInternalClient>"
		"<NewEnabled>1</NewEnabled>"
		"<NewPortMappingDescription>%s</NewPortMappingDescription>"
		"<NewLeaseDuration>%d</NewLeaseDuration>"
		"</u:%s></s:Body></s:Envelope>"
		, soap_action, d.service_namespace.c_str(), d.mapping[i].external_port
		, to_string(d.mapping[i].protocol)
		, d.mapping[i].local_ep.port()
		, local_endpoint.c_str()
		, m_settings.get_bool(settings_pack::anonymous_mode)
			? "" : m_settings.get_str(settings_pack::user_agent).c_str()
		, lease_duration(d), soap_action);

	post(d, soap, soap_action);
}

void upnp::next(rootdevice& d, port_mapping_t const i)
{
	TORRENT_ASSERT(is_single_thread());
	TORRENT_ASSERT(!d.disabled);
	if (i < prev(m_mappings.end_index()))
	{
		update_map(d, lt::next(i));
	}
	else
	{
		auto const j = std::find_if(d.mapping.begin(), d.mapping.end()
			, [](mapping_t const& m) { return m.act != portmap_action::none; });
		if (j == d.mapping.end()) return;

		update_map(d, port_mapping_t{static_cast<int>(j - d.mapping.begin())});
	}
}

void upnp::update_map(rootdevice& d, port_mapping_t const i)
{
	TORRENT_ASSERT(is_single_thread());
	TORRENT_ASSERT(d.magic == 1337);
	TORRENT_ASSERT(i < d.mapping.end_index());
	TORRENT_ASSERT(d.mapping.size() == m_mappings.size());
	TORRENT_ASSERT(!d.disabled);

	if (d.upnp_connection) return;

	// this should not happen, but in case it does, don't fail at runtime
	if (i >= d.mapping.end_index()) return;

	std::shared_ptr<upnp> me(self());

	mapping_t& m = d.mapping[i];

	if (m.act == portmap_action::none
		|| m.protocol == portmap_protocol::none)
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("mapping %d does not need updating, skipping", static_cast<int>(i));
#endif
		m.act = portmap_action::none;
		next(d, i);
		return;
	}

	TORRENT_ASSERT(!d.upnp_connection);
	TORRENT_ASSERT(!d.service_namespace.empty());

#ifndef TORRENT_DISABLE_LOGGING
	log("connecting to %s", d.hostname.c_str());
#endif
	if (m.act == portmap_action::add)
	{
		if (m.failcount > 5)
		{
			m.act = portmap_action::none;
			// giving up
			next(d, i);
			return;
		}

		if (d.upnp_connection) d.upnp_connection->close();
		d.upnp_connection = std::make_shared<http_connection>(m_io_service
			, m_resolver
			, std::bind(&upnp::on_upnp_map_response, self(), _1, _2
				, std::ref(d), i, _4), default_max_bottled_buffer_size
			, std::bind(&upnp::create_port_mapping, self(), _1, std::ref(d), i)
			, http_filter_handler()
			, hostname_filter_handler()
#if TORRENT_USE_SSL
			, &m_ssl_ctx
#endif
			);

		bind_info_t bi{m.device, m.local_ep.address()};
		d.upnp_connection->start(d.hostname, d.port
			, seconds(10), nullptr, false, 5, bi);
	}
	else if (m.act == portmap_action::del)
	{
		if (d.upnp_connection) d.upnp_connection->close();
		d.upnp_connection = std::make_shared<aux::http_connection>(m_io_service
			, m_resolver
			, std::bind(&upnp::on_upnp_unmap_response, self(), _1, _2
				, std::ref(d), i, _4), default_max_bottled_buffer_size
			, std::bind(&upnp::delete_port_mapping, self(), std::ref(d), i)
			, http_filter_handler()
			, hostname_filter_handler()
#if TORRENT_USE_SSL
			, &m_ssl_ctx
#endif
			);
		bind_info_t bi{m.device, m.local_ep.address()};
		d.upnp_connection->start(d.hostname, d.port
			, seconds(10), nullptr, false, 5, bi);
	}

	m.act = portmap_action::none;
	m.expires = aux::time_now() + seconds(30);
}

void upnp::delete_port_mapping(rootdevice& d, port_mapping_t const i)
{
	TORRENT_ASSERT(is_single_thread());

	TORRENT_ASSERT(d.magic == 1337);

	if (!d.upnp_connection)
	{
		TORRENT_ASSERT(d.disabled);
#ifndef TORRENT_DISABLE_LOGGING
		log("unmapping %d aborted", static_cast<int>(i));
#endif
		return;
	}

	char const* soap_action = "DeletePortMapping";

	char soap[1024];
	std::snprintf(soap, sizeof(soap), "<?xml version=\"1.0\"?>\n"
		"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
		"<s:Body><u:%s xmlns:u=\"%s\">"
		"<NewRemoteHost></NewRemoteHost>"
		"<NewExternalPort>%d</NewExternalPort>"
		"<NewProtocol>%s</NewProtocol>"
		"</u:%s></s:Body></s:Envelope>"
		, soap_action, d.service_namespace.c_str()
		, d.mapping[i].external_port
		, to_string(d.mapping[i].protocol)
		, soap_action);

	post(d, soap, soap_action);
}

void find_control_url(int const type, string_view str, parse_state& state)
{
	if (type == xml_start_tag)
	{
		state.tag_stack.push_back(str);
	}
	else if (type == xml_end_tag)
	{
		if (!state.tag_stack.empty())
		{
			if (state.in_service && string_equal_no_case(state.tag_stack.back(), "service"))
				state.in_service = false;
			state.tag_stack.pop_back();
		}
	}
	else if (type == xml_string)
	{
		if (state.tag_stack.empty()) return;
		// default to the first (or only) control url in the router's listing
		if (!state.in_service
			&& state.top_tags("service", "servicetype")
			&& state.service_type.empty())
		{
			if (string_equal_no_case(str, "urn:schemas-upnp-org:service:WANIPConnection:1")
				|| string_equal_no_case(str, "urn:schemas-upnp-org:service:WANIPConnection:2")
				|| string_equal_no_case(str, "urn:schemas-upnp-org:service:WANPPPConnection:1"))
			{
				state.service_type.assign(str.begin(), str.end());
				state.in_service = true;
			}
		}
		else if (state.control_url.empty() && state.in_service
			&& state.top_tags("service", "controlurl") && str.size() > 0)
		{
			// default to the first (or only) control url in the router's listing
			state.control_url.assign(str.begin(), str.end());
		}
		else if (state.model.empty() && state.top_tags("device", "modelname"))
		{
			state.model.assign(str.begin(), str.end());
		}
		else if (string_equal_no_case(state.tag_stack.back(), "urlbase"))
		{
			state.url_base.assign(str.begin(), str.end());
		}
	}
}

void upnp::on_upnp_xml(error_code const& e
	, aux::http_parser const& p, rootdevice& d
	, aux::http_connection& c)
{
	TORRENT_ASSERT(is_single_thread());
	std::shared_ptr<upnp> me(self());

	TORRENT_ASSERT(d.magic == 1337);
	if (d.upnp_connection && d.upnp_connection.get() == &c)
	{
		d.upnp_connection->close();
		d.upnp_connection.reset();
	}

	if (m_closing) return;

	if (e && e != boost::asio::error::eof)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while fetching control url from: %s: %s"
				, d.url.c_str(), e.message().c_str());
		}
#endif
		d.disabled = true;
		return;
	}

	if (!p.header_finished())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while fetching control url from: %s: incomplete HTTP message"
			, d.url.c_str());
#endif
		d.disabled = true;
		return;
	}

	if (p.status_code() != 200)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while fetching control url from: %s: %s"
				, d.url.c_str(), p.message().c_str());
		}
#endif
		d.disabled = true;
		return;
	}

	parse_state s;
	auto body = p.get_body();
	xml_parse({body.data(), std::size_t(body.size())}, std::bind(&find_control_url, _1, _2, std::ref(s)));
	if (s.control_url.empty())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("could not find a port mapping interface in response from: %s"
			, d.url.c_str());
#endif
		d.disabled = true;
		return;
	}
	d.service_namespace = s.service_type;

	TORRENT_ASSERT(!d.service_namespace.empty());

	if (!s.model.empty()) m_model = s.model;

	if (!s.url_base.empty() && s.control_url.substr(0, 7) != "http://")
	{
		// avoid double slashes in path
		if (s.url_base[s.url_base.size()-1] == '/'
			&& !s.control_url.empty()
			&& s.control_url[0] == '/')
			s.url_base.erase(s.url_base.end()-1);
		d.control_url = s.url_base + s.control_url;
	}
	else d.control_url = s.control_url;

	std::string protocol;
	std::string auth;
	error_code ec;
	if (!d.control_url.empty() && d.control_url[0] == '/')
	{
		std::tie(protocol, auth, d.hostname, d.port, d.path)
			= parse_url_components(d.url, ec);
		if (d.port == -1) d.port = protocol == "http" ? 80 : 443;
		d.control_url = protocol + "://" + d.hostname + ":"
			+ to_string(d.port).data() + s.control_url;
	}

#ifndef TORRENT_DISABLE_LOGGING
	if (should_log())
	{
		log("found control URL: %s namespace %s "
			"urlbase: %s in response from %s"
			, d.control_url.c_str(), d.service_namespace.c_str()
			, s.url_base.c_str(), d.url.c_str());
	}
#endif

	std::tie(protocol, auth, d.hostname, d.port, d.path)
		= parse_url_components(d.control_url, ec);
	if (d.port == -1) d.port = protocol == "http" ? 80 : 443;

	if (ec)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("failed to parse URL '%s': %s"
				, d.control_url.c_str(), ec.message().c_str());
		}
#endif
		d.disabled = true;
		return;
	}

	if (d.upnp_connection) d.upnp_connection->close();
	d.upnp_connection = std::make_shared<http_connection>(m_io_service
		, m_resolver
		, std::bind(&upnp::on_upnp_get_ip_address_response, self(), _1, _2
			, std::ref(d), _4), default_max_bottled_buffer_size
		, std::bind(&upnp::get_ip_address, self(), std::ref(d))
		, http_filter_handler()
		, hostname_filter_handler()
#if TORRENT_USE_SSL
		, &m_ssl_ctx
#endif
		);
	d.upnp_connection->start(d.hostname, d.port, seconds(10));
}

void upnp::get_ip_address(rootdevice& d)
{
	TORRENT_ASSERT(is_single_thread());

	TORRENT_ASSERT(d.magic == 1337);

	if (!d.upnp_connection)
	{
		TORRENT_ASSERT(d.disabled);
#ifndef TORRENT_DISABLE_LOGGING
		log("getting external IP address");
#endif
		return;
	}

	char const* soap_action = "GetExternalIPAddress";

	char soap[1024];
	std::snprintf(soap, sizeof(soap), "<?xml version=\"1.0\"?>\n"
		"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
		"<s:Body><u:%s xmlns:u=\"%s\">"
		"</u:%s></s:Body></s:Envelope>"
		, soap_action, d.service_namespace.c_str()
		, soap_action);

	post(d, soap, soap_action);
}

void upnp::disable(error_code const& ec)
{
	TORRENT_ASSERT(is_single_thread());
	m_disabled = true;

	// kill all mappings
	for (auto i = m_mappings.begin(), end(m_mappings.end()); i != end; ++i)
	{
		if (i->protocol == portmap_protocol::none) continue;
		portmap_protocol const proto = i->protocol;
		i->protocol = portmap_protocol::none;
		m_callback.on_port_mapping(port_mapping_t(static_cast<int>(i - m_mappings.begin()))
			, address(), 0, proto, ec, portmap_transport::upnp, m_listen_handle);
	}

	// we cannot clear the devices since there
	// might be outstanding requests relying on
	// the device entry being present when they
	// complete
	m_broadcast_timer.cancel();
	m_refresh_timer.cancel();
	m_map_timer.cancel();
	error_code e;
	m_unicast.socket.close(e);
	m_multicast.socket.close(e);
}

void find_error_code(int const type, string_view string, error_code_parse_state& state)
{
	if (state.exit) return;
	if (type == xml_start_tag && string == "errorCode")
	{
		state.in_error_code = true;
	}
	else if (type == xml_string && state.in_error_code)
	{
		state.error_code = std::atoi(std::string(string).c_str());
		state.exit = true;
	}
}

void find_ip_address(int const type, string_view string, ip_address_parse_state& state)
{
	find_error_code(type, string, state);
	if (state.exit) return;

	if (type == xml_start_tag && string == "NewExternalIPAddress")
	{
		state.in_ip_address = true;
	}
	else if (type == xml_string && state.in_ip_address)
	{
		state.ip_address.assign(string.begin(), string.end());
		state.exit = true;
	}
}

namespace {

	struct error_code_t
	{
		int code;
		char const* msg;
	};

	error_code_t error_codes[] =
	{
		{0, "no error"}
		, {402, "Invalid Arguments"}
		, {501, "Action Failed"}
		, {714, "The specified value does not exist in the array"}
		, {715, "The source IP address cannot be wild-carded"}
		, {716, "The external port cannot be wild-carded"}
		, {718, "The port mapping entry specified conflicts with "
			"a mapping assigned previously to another client"}
		, {724, "Internal and External port values must be the same"}
		, {725, "The NAT implementation only supports permanent "
			"lease times on port mappings"}
		, {726, "RemoteHost must be a wildcard and cannot be a "
			"specific IP address or DNS name"}
		, {727, "ExternalPort must be a wildcard and cannot be a specific port "}
	};

}

struct upnp_error_category final : boost::system::error_category
{
	const char* name() const BOOST_SYSTEM_NOEXCEPT override
	{
		return "upnp";
	}

	std::string message(int ev) const override
	{
		int num_errors = sizeof(error_codes) / sizeof(error_codes[0]);
		error_code_t* end = error_codes + num_errors;
		error_code_t tmp = {ev, nullptr};
		error_code_t* e = std::lower_bound(error_codes, end, tmp
			, [] (error_code_t const& lhs, error_code_t const& rhs)
			{ return lhs.code < rhs.code; });
		if (e != end && e->code == ev)
		{
			return e->msg;
		}
		char msg[500];
		std::snprintf(msg, sizeof(msg), "unknown UPnP error (%d)", ev);
		return msg;
	}

	boost::system::error_condition default_error_condition(
		int ev) const BOOST_SYSTEM_NOEXCEPT override
	{
		return {ev, *this};
	}
};

boost::system::error_category& upnp_category()
{
	static upnp_error_category cat;
	return cat;
}

void upnp::on_upnp_get_ip_address_response(error_code const& e
	, aux::http_parser const& p, rootdevice& d
	, http_connection& c)
{
	TORRENT_ASSERT(is_single_thread());
	std::shared_ptr<upnp> me(self());

	TORRENT_ASSERT(d.magic == 1337);
	if (d.upnp_connection && d.upnp_connection.get() == &c)
	{
		d.upnp_connection->close();
		d.upnp_connection.reset();
	}

	if (m_closing) return;

	if (e && e != boost::asio::error::eof)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while getting external IP address: %s"
				, e.message().c_str());
		}
#endif
		if (num_mappings() > 0) update_map(d, port_mapping_t{0});
		return;
	}

	if (!p.header_finished())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while getting external IP address: incomplete http message");
#endif
		if (num_mappings() > 0) update_map(d, port_mapping_t{0});
		return;
	}

	if (p.status_code() != 200)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while getting external IP address: %s"
				, p.message().c_str());
		}
#endif
		if (num_mappings() > 0) update_map(d, port_mapping_t{0});
		return;
	}

	// response may look like
	// <?xml version="1.0"?>
	// <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
	// <s:Body><u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
	// <NewExternalIPAddress>192.168.160.19</NewExternalIPAddress>
	// </u:GetExternalIPAddressResponse>
	// </s:Body>
	// </s:Envelope>

	span<char const> body = p.get_body();
#ifndef TORRENT_DISABLE_LOGGING
	if (should_log())
	{
		log("get external IP address response: %s"
			, std::string(body.data(), static_cast<std::size_t>(body.size())).c_str());
	}
#endif

	ip_address_parse_state s;
	xml_parse({body.data(), std::size_t(body.size())}, std::bind(&find_ip_address, _1, _2, std::ref(s)));
#ifndef TORRENT_DISABLE_LOGGING
	if (s.error_code != -1)
	{
		log("error while getting external IP address, code: %d", s.error_code);
	}
#endif

	if (!s.ip_address.empty())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("got router external IP address %s", s.ip_address.c_str());
#endif
		d.external_ip = make_address(s.ip_address.c_str(), ignore_error);
	}
	else
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("failed to find external IP address in response");
#endif
	}

	if (num_mappings() > 0) update_map(d, port_mapping_t{0});
}

void upnp::on_upnp_map_response(error_code const& e
	, aux::http_parser const& p, rootdevice& d, port_mapping_t const mapping
	, http_connection& c)
{
	TORRENT_ASSERT(is_single_thread());
	std::shared_ptr<upnp> me(self());

	TORRENT_ASSERT(d.magic == 1337);
	if (d.upnp_connection && d.upnp_connection.get() == &c)
	{
		d.upnp_connection->close();
		d.upnp_connection.reset();
	}

	if (e && e != boost::asio::error::eof)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while adding port map: %s"
				, e.message().c_str());
		}
#endif
		d.disabled = true;
		return;
	}

	if (m_closing) return;

//	 error code response may look like this:
//	<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
//		s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
//	 <s:Body>
//	  <s:Fault>
//		<faultcode>s:Client</faultcode>
//		<faultstring>UPnPError</faultstring>
//		<detail>
//		 <UPnPErrorxmlns="urn:schemas-upnp-org:control-1-0">
//		  <errorCode>402</errorCode>
//		  <errorDescription>Invalid Args</errorDescription>
//		 </UPnPError>
//		</detail>
//	  </s:Fault>
//	 </s:Body>
//	</s:Envelope>

	if (!p.header_finished())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while adding port map: incomplete http message");
#endif
		next(d, mapping);
		return;
	}

	std::string const& ct = p.header("content-type");
	if (!ct.empty() && !aux::is_upnp_xml_content_type(ct))
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while adding port map: invalid content-type, \"%s\". "
			"Expected text/xml or application/soap+xml", ct.c_str());
#endif
		next(d, mapping);
		return;
	}

	// We don't want to ignore responses with return codes other than 200
	// since those might contain valid UPnP error codes

	error_code_parse_state s;
	span<char const> body = p.get_body();
	xml_parse({body.data(), std::size_t(body.size())}, std::bind(&find_error_code, _1, _2, std::ref(s)));

	if (s.error_code != -1)
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while adding port map, code: %d", s.error_code);
#endif
	}

	mapping_t& m = d.mapping[mapping];

	if (s.error_code == 725)
	{
		// The gateway only supports permanent leases
		d.use_lease_duration = false;
		m.act = portmap_action::add;
		++m.failcount;
		update_map(d, mapping);
		return;
	}
	else if (s.error_code == 727)
	{
		return_error(mapping, s.error_code);
	}
	else if ((s.error_code == 718 || s.error_code == 501) && m.failcount < 4)
	{
		// some routers return 501 action failed, instead of 716
		// The external port conflicts with another mapping
		// pick a random port
		m.external_port = 40000 + int(random(10000));
		m.act = portmap_action::add;
		++m.failcount;
		update_map(d, mapping);
		return;
	}
	else if (s.error_code != -1)
	{
		return_error(mapping, s.error_code);
	}

#ifndef TORRENT_DISABLE_LOGGING
	if (should_log())
	{
		log("map response: %s"
			, std::string(body.data(), static_cast<std::size_t>(body.size())).c_str());
	}
#endif

	if (s.error_code == -1)
	{
		m_callback.on_port_mapping(mapping, d.external_ip, m.external_port, m.protocol, error_code()
			, portmap_transport::upnp, m_listen_handle);
		if (d.use_lease_duration && m_settings.get_int(settings_pack::upnp_lease_duration) != 0)
		{
			time_point const now = aux::time_now();
			m.expires = now + seconds(
				m_settings.get_int(settings_pack::upnp_lease_duration) * 3 / 4);
			time_point next_expire = m_refresh_timer.expiry();
			if (next_expire < now || next_expire > m.expires)
			{
				ADD_OUTSTANDING_ASYNC("upnp::on_expire");
				m_refresh_timer.expires_at(m.expires);
				m_refresh_timer.async_wait(std::bind(&upnp::on_expire, self(), _1));
			}
		}
		else
		{
			m.expires = max_time();
		}
		m.failcount = 0;
	}

	next(d, mapping);
}

void upnp::return_error(port_mapping_t const mapping, int const code)
{
	TORRENT_ASSERT(is_single_thread());
	int num_errors = sizeof(error_codes) / sizeof(error_codes[0]);
	error_code_t* end = error_codes + num_errors;
	error_code_t tmp = {code, nullptr};
	error_code_t* e = std::lower_bound(error_codes, end, tmp
		, [] (error_code_t const& lhs, error_code_t const& rhs)
		{ return lhs.code < rhs.code; });

	std::string error_string = "UPnP mapping error ";
	error_string += to_string(code).data();
	if (e != end && e->code == code)
	{
		error_string += ": ";
		error_string += e->msg;
	}
	portmap_protocol const proto = m_mappings[mapping].protocol;
	m_callback.on_port_mapping(mapping, address(), 0, proto, error_code(code, upnp_category())
		, portmap_transport::upnp, m_listen_handle);
}

void upnp::on_upnp_unmap_response(error_code const& e
	, aux::http_parser const& p, rootdevice& d
	, port_mapping_t const mapping
	, http_connection& c)
{
	TORRENT_ASSERT(is_single_thread());
	std::shared_ptr<upnp> me(self());

	TORRENT_ASSERT(d.magic == 1337);
	if (d.upnp_connection && d.upnp_connection.get() == &c)
	{
		d.upnp_connection->close();
		d.upnp_connection.reset();
	}

	if (e && e != boost::asio::error::eof)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while deleting portmap: %s"
				, e.message().c_str());
		}
#endif
	}
	else if (!p.header_finished())
	{
#ifndef TORRENT_DISABLE_LOGGING
		log("error while deleting portmap: incomplete http message");
#endif
	}
	else if (p.status_code() != 200)
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			log("error while deleting portmap: %s"
				, p.message().c_str());
		}
#endif
	}
	else
	{
#ifndef TORRENT_DISABLE_LOGGING
		if (should_log())
		{
			span<char const> body = p.get_body();
			log("unmap response: %s"
				, std::string(body.data(), static_cast<std::size_t>(body.size())).c_str());
		}
#endif
	}

	error_code_parse_state s;
	if (p.header_finished())
	{
		span<char const> body = p.get_body();
		xml_parse({body.data(), std::size_t(body.size())}, std::bind(&find_error_code, _1, _2, std::ref(s)));
	}

	portmap_protocol const proto = m_mappings[mapping].protocol;

	m_callback.on_port_mapping(mapping, address(), 0, proto, p.status_code() != 200
		? error_code(p.status_code(), http_category())
		: error_code(s.error_code, upnp_category())
		, portmap_transport::upnp, m_listen_handle);

	d.mapping[mapping].protocol = portmap_protocol::none;

	// free the slot in global mappings
	auto pred = [mapping](rootdevice const& rd)
		{ return rd.mapping.end_index() <= mapping || rd.mapping[mapping].protocol == portmap_protocol::none; };
	if (std::all_of(m_devices.begin(), m_devices.end(), pred))
	{
		m_mappings[mapping].protocol = portmap_protocol::none;
	}

	next(d, mapping);
}

void upnp::on_expire(error_code const& ec)
{
	TORRENT_ASSERT(is_single_thread());
	COMPLETE_ASYNC("upnp::on_expire");
	if (ec) return;

	if (m_closing) return;

	time_point const now = aux::time_now();
	time_point next_expire = max_time();

	for (auto const& dev : m_devices)
	{
		auto& d = const_cast<rootdevice&>(dev);
		TORRENT_ASSERT(d.magic == 1337);
		if (d.disabled) continue;
		for (port_mapping_t m{0}; m < m_mappings.end_index(); ++m)
		{
			if (d.mapping[m].expires == max_time())
				continue;

			if (d.mapping[m].expires <= now)
			{
				d.mapping[m].act = portmap_action::add;
				update_map(d, m);
			}
			if (d.mapping[m].expires < next_expire)
			{
				next_expire = d.mapping[m].expires;
			}
		}
	}
	if (next_expire != max_time())
	{
		ADD_OUTSTANDING_ASYNC("upnp::on_expire");
		m_refresh_timer.expires_at(next_expire);
		m_refresh_timer.async_wait(std::bind(&upnp::on_expire, self(), _1));
	}
}

void upnp::close()
{
	TORRENT_ASSERT(is_single_thread());

	m_refresh_timer.cancel();
	m_broadcast_timer.cancel();
	m_map_timer.cancel();
	m_closing = true;
	error_code ec;
	m_unicast.socket.close(ec);
	m_multicast.socket.close(ec);

	for (auto const& dev : m_devices)
	{
		auto& d = const_cast<rootdevice&>(dev);
		TORRENT_ASSERT(d.magic == 1337);
		if (d.disabled || d.control_url.empty()) continue;
		for (auto& m : d.mapping)
		{
			if (m.protocol == portmap_protocol::none) continue;
			if (m.act == portmap_action::add)
			{
				m.act = portmap_action::none;
				continue;
			}
			m.act = portmap_action::del;
			m_mappings[port_mapping_t{static_cast<int>(&m - d.mapping.data())}].protocol = portmap_protocol::none;
		}
		if (num_mappings() > 0) update_map(d, port_mapping_t{0});
	}
}

}
