// DWBS ------------------------------------------------------------------ DWBS
//
// PLATFORM		: WIN32 PS2 PS3 PSP-INFRA UNIX OSX
// PRODUCT		: BIT_DEMON
// VISIBILITY	: PUBLIC
//
// DWBS ------------------------------------------------------------------ DWBS

// PURPOSE: UPnP Nat traversal module

#include <bdNet/bdUPnP.h>
#include <bdNet/bdUPnPListener.h>

#define BD_LOG_LEVEL "bdnet/upnp"

// max characters present in an address string with null, e.g. "123.567.901.345:78901 "
#define MAX_ADDR_STRING 22

// maximum amount of time to wait for a router to respond during each phase
// of UPnP traversal except discovery which uses m_discoveryDuration
#define MAX_WAIT 10.f

// two different possible UPnP routing methods
static const bdByte8 IPService[] = "WANIPConnection:1";
static const bdByte8 PPPService[] = "WANPPPConnection:1";

#define BD_FIRE_LISTENER(l)								\
	do													\
{														\
	for(bdUInt i = 0; i < m_listeners.getSize(); i++)	\
{														\
	m_listeners[i]->l;									\
}														\
}														\
	while(g_assertFalse)

void bdUPnP::startDiscovery()
{
	if (m_discoveryDuration == 0)
	{
		m_timer.reset();
		m_UPnPState = BD_NONE_DISCOVERED;
		return;
	}

	// if this is the initial discovery the socket needs setting up
	if (m_UPnPState == BD_INITIAL)
	{
		// create a non-blocking UDP socket for UPnP discovery
		if (!m_discoverySocket.create())
		{
			m_timer.reset();
			m_UPnPState = BD_FAILED;
			m_errorCode = BD_NET_ERROR;
			BD_FIRE_LISTENER(onUPnPError());
			return;
		}

		// bind to any arbitrary port
		const bdPort defaultPort(0);
		bdSocketStatusCode errorCode = m_discoverySocket.bind(defaultPort);
		
		if (errorCode != BD_NET_SUCCESS)
		{
			m_timer.reset();
			m_UPnPState = BD_FAILED;
			m_errorCode = errorCode;
			BD_FIRE_LISTENER(onUPnPError());
			return;
		}
	}

	// set up the discovery request strings
	const bdByte8 discoveryString[] =
		"M-SEARCH * HTTP/1.1\r\n"
		"MX: 1\r\n" // ask the device to reply as quickly as possible
		"HOST: 239.255.255.250:1900\r\n"
		"MAN: \"ssdp:discover\"\r\n"
		"ST: urn:schemas-upnp-org:service:%s\r\n"
		"\r\n";

	// hold the discovery messages
	bdByte8 messageBuffer[sizeof(discoveryString) + sizeof(PPPService)];

	// discover both IP connection services and PPP connection services
	for (bdUInt loopCount = 0; loopCount < 2; loopCount++)
	{
		bdInt discLength = bdSnprintf(messageBuffer, sizeof(messageBuffer), 
										   discoveryString,
										   (loopCount == 0 ? IPService : PPPService) );

		// special reserved address for UPnP discovery
		const bdAddr discoveryAddr("239.255.255.250:1900");
		bdInt bytesSent = m_discoverySocket.sendTo(discoveryAddr, messageBuffer, static_cast<bdUInt>(discLength));
		
		if (bytesSent != discLength)
		{
			m_timer.reset();
			m_errorCode = bdSocketStatusCode(bytesSent);
			m_UPnPState = BD_FAILED;
			BD_FIRE_LISTENER(onUPnPError());
			return;
		}
	}

	m_timer.start();
	m_UPnPState = BD_DISCOVERING;
}

void bdUPnP::queryMapping(bdPort externalPort, const bdArray<bdInetAddr> *localAddrs)
{
	BD_ASSERT(m_UPnPState == BD_READY || m_UPnPState == BD_FAILED || m_UPnPState == BD_COLLISION_FOUND,
		"bdUPnP is not ready to query mappings");

	if (!m_deviceList.isEmpty())
	{
		bdLinkedList<bdUPnPDevice>::Position position = m_deviceList.getHeadPosition();

		while (position != BD_NULL)
		{
			bdUPnPDevice& currDevice = m_deviceList.forward(position); // advance iterator 

			if (currDevice.m_state == bdUPnPDevice::BD_READY || 
				currDevice.m_state == bdUPnPDevice::BD_MAPPED)
			{
				// connect to the TCP control port
				bdStreamSocket& currConnection = currDevice.m_connection;
				if (!currDevice.m_connection.create())
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
				
				if (!currDevice.m_connection.setBlocking(false))
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}

				bdSocketStatusCode sockStatus = currConnection.connect(currDevice.m_connectionAddr);

				// since the socket will take a few cycles to connect,
				// save the http message for later sending
				if (sockStatus == BD_NET_SUCCESS || sockStatus == BD_NET_WOULD_BLOCK)
				{
					// generate port map command
					const bdByte8 mapPort[] = "GetSpecificPortMappingEntry";
					const bdByte8 arguments[] =
						"<NewRemoteHost></NewRemoteHost>\r\n"
						"<NewExternalPort>%d</NewExternalPort>\r\n"
						"<NewProtocol>UDP</NewProtocol>\r\n";

					// allocate space for the control string and the filled-in port value
					const bdUInt maxArgLength = bdUInt(bdStrlen(arguments) + 5);
					bdByte8* queryMapArguments = bdAllocate<bdByte8>(maxArgLength);

					bdSnprintf(queryMapArguments, maxArgLength, arguments, externalPort);

					BD_ASSERT(currDevice.m_httpRequest == BD_NULL, "http request was not destroyed");
					currDevice.m_httpRequest = genUPnPCommand(currDevice, mapPort, queryMapArguments);

					//BD_INFO(BD_LOG_LEVEL, "\nUPnP query sent:\n%s\n", currDevice->m_httpRequest);

					bdDeallocate<bdByte8>(queryMapArguments);

					currDevice.m_state = bdUPnPDevice::BD_CONNECT_QUERY;
				}
				else
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = sockStatus;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
			}
		}

		// so long as a device error hasn't occurred within this function, 
		// bdUPnP is now querying the mapping table
		if ( m_UPnPState != BD_FAILED)
		{
			lastPortQueried = externalPort;

			// track the local addresses to check for existing usable port mappings
			m_localAddrs = localAddrs;
			
			m_timer.start();
			m_UPnPState = BD_QUERYING;
		}
	}
}

void bdUPnP::openPort(bdPort localPort, const bdInAddr localAddr)
{
	BD_ASSERT(m_UPnPState == BD_READY || m_UPnPState == BD_FAILED,
		"bdUPnP is not ready to open ports");

	bdByte8 addrString[MAX_ADDR_STRING];
	localAddr.toString(addrString, MAX_ADDR_STRING);

	if (!m_deviceList.isEmpty())
	{
		bdLinkedList<bdUPnPDevice>::Position position = m_deviceList.getHeadPosition();

		while (position != BD_NULL)
		{
			bdUPnPDevice& currDevice = m_deviceList.forward(position); // advance iterator

			if (currDevice.m_state == bdUPnPDevice::BD_READY || 
				currDevice.m_state == bdUPnPDevice::BD_MAPPED)
			{
				// connect to the TCP control port
				bdStreamSocket& currConnection = currDevice.m_connection;
				if (!currDevice.m_connection.create())
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
				
				if (!currDevice.m_connection.setBlocking(false))
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
				
				bdSocketStatusCode sockStatus = currConnection.connect(currDevice.m_connectionAddr);

				// since the socket will take a few cycles to connect,
				// save the http message for later sending
				if (sockStatus == BD_NET_SUCCESS || sockStatus == BD_NET_WOULD_BLOCK)
				{
					// generate port map command
					const bdByte8 mapPort[] = "AddPortMapping";
					const bdByte8 arguments[] =
						"      <NewRemoteHost></NewRemoteHost>\r\n"
						"      <NewExternalPort>%d</NewExternalPort>\r\n"
						"      <NewProtocol>UDP</NewProtocol>\r\n"
						"      <NewInternalPort>%d</NewInternalPort>\r\n"
						"      <NewInternalClient>%s</NewInternalClient>\r\n"
						"      <NewEnabled>1</NewEnabled>\r\n"
						"      <NewPortMappingDescription>"
						"DemonwarePortMapping"
						"</NewPortMappingDescription>\r\n"
						"      <NewLeaseDuration>0</NewLeaseDuration>\r\n";

					// allocate space for the control string and the filled-in port values
					const bdUInt maxArgLength = bdUInt(bdStrlen(arguments) 
													 + MAX_ADDR_STRING + 10);

					bdByte8* portMapArguments = bdAllocate<bdByte8>(maxArgLength);
					m_externalPortMap.addTail(localPort);

#if defined(BD_PLATFORM_WIN32)
					currDevice.m_localAddr.toString(addrString, MAX_ADDR_STRING);
#endif

					bdSnprintf(portMapArguments, maxArgLength, arguments, 
							   localPort, localPort, addrString);

					BD_ASSERT(currDevice.m_httpRequest == BD_NULL, "http request was not destroyed");
					
					currDevice.m_httpRequest = genUPnPCommand(currDevice, mapPort, portMapArguments);
					//BD_INFO(BD_LOG_LEVEL, "\nUPnP mapping request sent:\n%s\n", currDevice->m_httpRequest);

					bdDeallocate<bdByte8>(portMapArguments);

					currDevice.m_state = bdUPnPDevice::BD_CONNECT_MAP;

					const bdUInt32 tempPort = localPort;
					bdNChar8 addressBuffer[BD_ADDR_STRING_SIZE];
					currDevice.m_connectionAddr.toString(addressBuffer , BD_ADDR_STRING_SIZE);
					BD_INFO(BD_LOG_LEVEL, "Creating port mapping for local port %u on device at %s  ", tempPort, addressBuffer);
				
				}
				else
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = sockStatus;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
			}
		}

		// so long as a device error hasn't occurred within this function, 
		// bdUPnP is now opening ports
		if ( m_UPnPState != BD_FAILED)
		{
			m_timer.start();
			m_UPnPState = BD_OPENING;
		}
	}
}

void bdUPnP::clearPortMap()
{
	m_timer.start();
	m_UPnPState = BD_CLOSING;

	// removal of mappings must be done one at a time; this is the first iteration
	removeNextPortMap();
}

bdUPnP::bdUPnPState bdUPnP::getState() const
{
	return m_UPnPState;
}

bdUInt bdUPnP::getNumMappings() const
{
	return m_externalPortMap.getSize();
}

bdBool bdUPnP::isMapped(bdPort port)
{
	return (m_externalPortMap.find(port) != BD_NULL);
}

bdSocketStatusCode bdUPnP::getError() const
{
	return m_errorCode;
}

void bdUPnP::pump()
{
	if (m_UPnPState == BD_DISCOVERING)
	{
		bdAddr incomingAddress;
		const bdUInt MAX_DISC_REPLY = 1024;
		bdByte8 messageBuffer[MAX_DISC_REPLY + 1];
		static bdInt sockStatus = BD_NET_SUCCESS;
		
		sockStatus = m_discoverySocket.receiveFrom(incomingAddress, messageBuffer, MAX_DISC_REPLY);
		
		// a positive socket status indicates that a message of size sockStatus was received
		if (sockStatus > 0)
		{
			// null terminate the string
			messageBuffer[sockStatus] = 0;

			registerDevice(messageBuffer);

		}
		else if (sockStatus != BD_NET_WOULD_BLOCK)
		{
			m_timer.reset();
			m_UPnPState = BD_FAILED;
			m_discoverySocket.close();
			m_errorCode = bdSocketStatusCode(sockStatus);
			BD_FIRE_LISTENER(onUPnPError());
			return;
		}

		if (m_timer.getElapsedTimeInSeconds() >= m_discoveryDuration)
		{
			m_timer.reset();
			m_UPnPState = (m_deviceList.isEmpty() ? BD_NONE_DISCOVERED : BD_READY);
			m_discoverySocket.close();
			BD_FIRE_LISTENER(onDiscovery(m_deviceList.getSize()));
		}
	}
	// check for timeout on non-discovery related task
	else if (m_timer.getElapsedTimeInSeconds() >= MAX_WAIT)
	{
		// router is taking too long to respond, bail out
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		BD_FIRE_LISTENER(onUPnPError());
		return;
	}

	// check each registered device's state and handle it accordingly
	if (!m_deviceList.isEmpty())
	{
		bdLinkedList<bdUPnPDevice>::Position position = m_deviceList.getHeadPosition();

		// because of the mammoth memory requirements of a buffer to hold the UPnP traffic,
		// especially the UPnP description, only one buffer exists at a time for TCP reads, 
		// thus, only one socket can be read at a time. buffInUse marks whether a TCP socket
		// has already been polled this frame.
		bdBool buffInUse = false;
		
		while (position != BD_NULL && m_UPnPState != BD_FAILED)
		{
			bdUPnPDevice& currDevice = m_deviceList.forward(position); // advance iterator

			if (!buffInUse && currDevice.m_state == bdUPnPDevice::BD_PORT_QUERYING)
			{
				buffInUse = true;

				// parse the reply from the port mapping request
				readQueryReply(currDevice);
			}
			else if (!buffInUse && currDevice.m_state == bdUPnPDevice::BD_MAPPING)
			{
				buffInUse = true;

				// parse the reply from the port mapping request
				readMapReply(currDevice, false);
			}
			else if (!buffInUse && currDevice.m_state == bdUPnPDevice::BD_UNMAPPING)
			{
				buffInUse = true;

				// parse the reply from the unmapping request
				readMapReply(currDevice, true);
			}
			else if (!buffInUse && currDevice.m_state == bdUPnPDevice::BD_FETCHING_DESC)
			{
				// ensure that no other descriptions taint the TCP buffer
				buffInUse = true;

				// read and parse the description
				readDescription(currDevice);
			}
			else if (currDevice.m_state == bdUPnPDevice::BD_CONNECT_DESC ||
					currDevice.m_state == bdUPnPDevice::BD_CONNECT_QUERY ||
					currDevice.m_state == bdUPnPDevice::BD_CONNECT_MAP ||
					currDevice.m_state == bdUPnPDevice::BD_CONNECT_UNMAP)
			{
				// send the http request to device's control socket
				const bdUInt dataSize = bdUInt(bdStrlen(currDevice.m_httpRequest));
				bdInt sockStatus = currDevice.m_connection.send(currDevice.m_httpRequest, dataSize);

				// check if data was successfully sent
				if (sockStatus == static_cast<bdInt>(dataSize))
				{
					bdDeallocate<bdByte8>(currDevice.m_httpRequest);
					currDevice.m_httpRequest = BD_NULL;

					// advance device state
					if (currDevice.m_state == bdUPnPDevice::BD_CONNECT_DESC)
					{
						currDevice.m_state = bdUPnPDevice::BD_FETCHING_DESC;
					}
					else if (currDevice.m_state == bdUPnPDevice::BD_CONNECT_QUERY)
					{
						currDevice.m_state = bdUPnPDevice::BD_PORT_QUERYING;
					}
					else if (currDevice.m_state == bdUPnPDevice::BD_CONNECT_MAP)
					{
						currDevice.m_state = bdUPnPDevice::BD_MAPPING;
					}
					else // currDevice.state == BD_CONNECT_UNMAP
					{
						currDevice.m_state = bdUPnPDevice::BD_UNMAPPING;
					}
				}
				else if (sockStatus != BD_NET_NOT_CONNECTED && sockStatus != BD_NET_WOULD_BLOCK)
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					m_errorCode = bdSocketStatusCode(sockStatus);
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
			}
		}
	}
}

void bdUPnP::setDiscoveryTimeOut(bdUInt seconds)
{
	m_discoveryDuration = bdFloat32(seconds);
}

bdUPnP::bdUPnP() :
m_UPnPState(BD_INITIAL),
m_errorCode(BD_NET_SUCCESS),
m_readBuffer(BD_NULL),
m_readBufferOffset(0),
m_discoveryDuration(3.0f)
{
}

bdUPnP::~bdUPnP()
{
	if (m_readBuffer)
	{
		bdDeallocate<bdByte8>(m_readBuffer);
		m_readBuffer = BD_NULL;
	}
}

void bdUPnP::registerDevice(bdByte8* const discoveryResponse)
{
	const bdByte8 HTTP[] = "http://";

	// search the response for the device URL starting with "http://"
	bdByte8 *httpString = bdStrstr(discoveryResponse, HTTP);

	if (httpString == BD_NULL)
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		BD_WARN(BD_LOG_LEVEL, "no http:// token found in discovery response");
		BD_FIRE_LISTENER(onUPnPError());
		return;
	}

	// advance to the actual address which follows "http://"
	httpString += strlen(HTTP);

	// find the / character following the address
	// to deliminate address from fetch location
	bdByte8 *slashLoc = bdStrchr(httpString, '/');

	if (slashLoc == BD_NULL)
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		BD_WARN(BD_LOG_LEVEL, "No fetch path found after URL");
		BD_FIRE_LISTENER(onUPnPError());
		return;
	}

	// grab address string which occurs between "http://" and the first /
	bdUInt addrStringLength = bdUInt(slashLoc - httpString);

	bdByte8 addrString[MAX_ADDR_STRING];
	bdStrlcpy(addrString, httpString, addrStringLength + 1);
	addrString[addrStringLength] = 0;

	// make sure this discovery response isn't from an already registered router
	if (!m_deviceList.isEmpty())
	{
		bdAddr discoveredAddress(addrString);
		bdLinkedList<bdUPnPDevice>::Position devicePos = m_deviceList.getHeadPosition();

		while (devicePos != BD_NULL)
		{
			bdUPnPDevice& existingDevice = m_deviceList.forward(devicePos);
			if (existingDevice.m_connectionAddr == discoveredAddress)
			{
				// device is already registered, no need to continue
				return;
			}
		}
	}

	// grab fetch location string which occurs after
	// the address and before the line ends
	bdByte8 *lineEnd = bdStrchr(httpString, '\r');
	if (lineEnd == BD_NULL)
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		BD_WARN(BD_LOG_LEVEL, "No carriage return after fetch location");
		BD_FIRE_LISTENER(onUPnPError());
		return;
	}
	
	bdUInt fetchLocationLength = bdUInt( lineEnd - slashLoc );
	const bdUInt FETCH_BUFF_SIZE = fetchLocationLength +1; // room for NULL terminator
	
	bdByte8* fetchLocation = bdAllocate<bdByte8>(FETCH_BUFF_SIZE);
	bdStrlcpy(fetchLocation, slashLoc, FETCH_BUFF_SIZE);
	fetchLocation[fetchLocationLength] = 0;

	// fetch location and address seem to be valid so 
	// create a bdUPnP device for this discovery
	bdUPnPDevice discoveredDevice;
	bdStreamSocket& discoveredConnection = discoveredDevice.m_connection;

	// establish connected TCP socket to given address
	discoveredDevice.m_connectionAddr.set(addrString);
	
	if (!discoveredConnection.create())
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		m_errorCode = BD_NET_ERROR;
		BD_FIRE_LISTENER(onUPnPError());
		bdDeallocate(fetchLocation);
		fetchLocation = BD_NULL;
		return;
	}
	
	if (!discoveredConnection.setBlocking(false))
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		m_errorCode = BD_NET_ERROR;
		BD_FIRE_LISTENER(onUPnPError());
		bdDeallocate(fetchLocation);
		fetchLocation = BD_NULL;
		return;
	}

	bdSocketStatusCode sockStatus = discoveredConnection.connect(discoveredDevice.m_connectionAddr);
	
	// since the socket will take a few cycles to connect,
	// save the GET message for later sending
	if (sockStatus == BD_NET_SUCCESS || sockStatus == BD_NET_WOULD_BLOCK)
	{
		// send a get request to the description URL
		const bdByte8 getRequest[] = 
			"GET %s HTTP/1.1\r\n"
			"Connection: close\r\n"
			"Host: %s\r\n\r\n";

		const bdUInt GET_STR_SIZE = bdUInt(bdStrlen(getRequest) + 
			fetchLocationLength + addrStringLength);

		discoveredDevice.m_httpRequest = bdAllocate<bdByte8>(GET_STR_SIZE);
		bdSnprintf(discoveredDevice.m_httpRequest, GET_STR_SIZE, 
			       getRequest, fetchLocation, addrString);

		discoveredDevice.m_state = bdUPnPDevice::BD_CONNECT_DESC;

		m_deviceList.addTail(discoveredDevice);
		bdNChar8 addressBuffer[BD_ADDR_STRING_SIZE];
		discoveredDevice.m_connectionAddr.toString(addressBuffer , BD_ADDR_STRING_SIZE);
		BD_INFO(BD_LOG_LEVEL , "UPnP capable device discovered at %s, adding to device list" , addressBuffer);
	}
	else
	{
		m_errorCode = sockStatus;
		discoveredDevice.m_state = bdUPnPDevice::BD_FAILED;
		BD_FIRE_LISTENER(onUPnPError());
	}
	bdDeallocate<bdByte8>(fetchLocation);
}

void bdUPnP::readDescription(bdUPnPDevice& currDevice)
{
	// maximum number of bytes an XML device description can occupy
	const bdUInt MAX_DESCRIPTION_SIZE = 1024 * 6;

	// make sure a description buffer is allocated for this device
	if (m_readBuffer == BD_NULL)
	{
		// descBufferOffset should be reset when the last description read completed
		BD_ASSERT(m_readBufferOffset == 0, "m_readBufferOffset was not reset");

		m_readBuffer = bdAllocate<bdByte8>(MAX_DESCRIPTION_SIZE + 1);
	}

	// receive on the connect description socket
	bdInt bytesReceived = currDevice.m_connection.recv(m_readBuffer + m_readBufferOffset,
						  MAX_DESCRIPTION_SIZE - m_readBufferOffset);
	
	if (bytesReceived > 0)
	{
		// null terminate the buffer so we can use string search functions
		m_readBuffer[m_readBufferOffset + bytesReceived] = BD_NULL;

		// advance position in buffer to account for new data received
		m_readBufferOffset += bytesReceived;
	}
	else if (bytesReceived == BD_NET_CONNECTION_CLOSED)
	{
#ifdef BD_PLATFORM_WIN32
		// get the IP associated with this connection for the local
		// address parameter in port mapping.
		if (!currDevice.m_connection.getSocketAddr(currDevice.m_localAddr))
		{
			m_timer.reset();
			m_UPnPState = BD_FAILED;
			m_errorCode = BD_NET_ERROR;
			BD_FIRE_LISTENER(onUPnPError());
			return;
		}
#endif
		//BD_INFO(BD_LOG_LEVEL, "\nUPnP description:\n%s\n", m_readBuffer);
		
		// close connection on this end
		currDevice.m_connection.close();

		// pointer used to parse through the XML description
		bdByte8* xmlParser;

		// parse for URLBASE tag that may indicate a different address for services
		// and possibly a string to be prepended to the controlURL
		bdByte8* URLPrefix = BD_NULL;
		bdUInt prefixLength = 0;
		const bdByte8 URL_BASE[] = "<URLBase";
		
		xmlParser = bdStrstr(m_readBuffer, URL_BASE);
		if (xmlParser != BD_NULL)
		{
			// bypass any junk before close of the tag
			xmlParser = bdStrstr(xmlParser, ">");
			if (xmlParser == BD_NULL)
			{ // description is improperly formatted, bail out
				m_UPnPState = BD_FAILED;
				m_errorCode = BD_NET_ERROR;
				BD_FIRE_LISTENER(onUPnPError());
				return;
			}
			xmlParser++;

			// check for blank tag URLBase tag. Yes, some routers actually do this
			if (xmlParser[0] != '<')
			{
				const bdByte8 HTTP[] = "http://";
				bdByte8* addressStart = bdStrstr(xmlParser, HTTP);

				if (addressStart != xmlParser)
				{
					m_timer.reset();
					m_UPnPState = BD_FAILED;
					BD_WARN(BD_LOG_LEVEL, "URLBase does not begin with HTTP://");
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}

				// pull out address:port from URLBase
				addressStart += bdStrlen(HTTP);
				bdByte8* endTag = bdStrchr(addressStart, '<');
				bdByte8* slashLoc = bdStrchr(addressStart, '/');

				// if there is a string matching "/fetchloc" before the endtag it must
				// be prepended to the controlURL
				if (slashLoc < endTag)
				{
					prefixLength = bdUInt(endTag - slashLoc);
					URLPrefix = bdAllocate<bdByte8>(prefixLength + 1);
					bdStrlcpy(URLPrefix, slashLoc, prefixLength + 1);
				}

				bdByte8* addressEnd = (endTag < slashLoc ? endTag : slashLoc);

				const bdUInt addressLength = bdUInt(addressEnd - addressStart);

				bdByte8 addrString[MAX_ADDR_STRING];
				bdStrlcpy(addrString, addressStart, addressLength + 1);
				addrString[addressLength] = 0;

				// replace current address with this new one
				currDevice.m_connectionAddr.set(addrString);
			}
		}

		xmlParser = bdStrstr(m_readBuffer, IPService);

		if (xmlParser != BD_NULL)
		{
			currDevice.m_isIp = true; // note that the service type is IP
		}
		else
		{
			currDevice.m_isIp = false; // note that the service type is PPP
			xmlParser = bdStrstr(m_readBuffer, PPPService);
		}

		if (xmlParser == BD_NULL)
		{
			// something went wrong; previously advertised service is missing
			currDevice.m_state = bdUPnPDevice::BD_FAILED;

			if (URLPrefix)
			{
				bdDeallocate<bdByte8>(URLPrefix);
				URLPrefix = BD_NULL;
			}

			BD_FIRE_LISTENER(onUPnPError());
			return;
		}
		else
		{
			const bdByte8 CONTROL_URL[] = "<controlURL";
			xmlParser = bdStrstr(xmlParser, CONTROL_URL); // advance to <controlURL> token

			if (xmlParser == BD_NULL)
			{ // no controlURL specified; description is improperly formatted, bail out
				m_UPnPState = BD_FAILED;
				m_errorCode = BD_NET_ERROR;

				if (URLPrefix)
				{
					bdDeallocate<bdByte8>(URLPrefix);
					URLPrefix = BD_NULL;
				}

				BD_FIRE_LISTENER(onUPnPError());
				return;
			}

			// bypass any junk before close of the tag
			xmlParser = bdStrstr(xmlParser, ">");
			if (xmlParser == BD_NULL)
			{ // description is improperly formatted, bail out
				m_UPnPState = BD_FAILED;
				m_errorCode = BD_NET_ERROR;

				if (URLPrefix)
				{
					bdDeallocate<bdByte8>(URLPrefix);
					URLPrefix = BD_NULL;
				}

				BD_FIRE_LISTENER(onUPnPError());
				return;
			}
			xmlParser++;

			// controlURL might be a fully qualified address check for "http://"
			const bdByte8 HTTP[] = "http://";
			bdByte8* addressStart = bdStrstr(xmlParser, HTTP);

			// intentional comparison of pointers; not meant to be a string comparison
			if (addressStart == xmlParser)
			{
				// the controlURL value begins with "http//" and must be a fully qualified address
				addressStart += bdStrlen(HTTP); // bypass http//

				// pull out address:port from control URL
				bdByte8* addressEnd = bdStrchr(addressStart, '/');

				const bdUInt addressLength = bdUInt(addressEnd - addressStart);

				bdByte8 addrString[MAX_ADDR_STRING];
				bdStrlcpy(addrString, addressStart, addressLength + 1);
				addrString[addressLength] = 0;

				// replace current address with this new one
				currDevice.m_connectionAddr.set(addrString);

				// advance the parser past the address
				xmlParser = addressEnd;
			}

			bdByte8* urlEnd = bdStrchr(xmlParser, '<'); // find closing tag

			if (urlEnd == BD_NULL)
			{
				// closing tag missing, description was incomplete
				currDevice.m_state = bdUPnPDevice::BD_FAILED;
				BD_FIRE_LISTENER(onUPnPError());
				
				if (URLPrefix)
				{
					bdDeallocate<bdByte8>(URLPrefix);
					URLPrefix = BD_NULL;
				}

				return;
			}

			const bdUInt urlLength = bdUInt(urlEnd - xmlParser);

			// form the full m_controlURL from the prefix and given controlURL
			currDevice.m_controlURL = bdAllocate<bdByte8>(urlLength + prefixLength + 1);
			if (prefixLength)
			{
				bdStrlcpy(currDevice.m_controlURL, URLPrefix, prefixLength + 1);
			}
			bdStrlcpy(currDevice.m_controlURL + prefixLength, xmlParser, urlLength + 1);
			//BD_INFO(BD_LOG_LEVEL, "\n\nFull UPnP control URL parsed: %s\n", currDevice.m_controlURL);
			
			// device is now ready for port mapping
			currDevice.m_state = bdUPnPDevice::BD_READY;
		}

		// all useful information has been gleaned from the description; destroy it
		bdDeallocate<bdByte8>(m_readBuffer);
		m_readBuffer = BD_NULL;
		m_readBufferOffset = 0;

		if (URLPrefix)
		{
			bdDeallocate<bdByte8>(URLPrefix);
			URLPrefix = BD_NULL;
		}
	}
	else if (bytesReceived != BD_NET_WOULD_BLOCK)
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		m_errorCode = bdSocketStatusCode(bytesReceived);
		BD_FIRE_LISTENER(onUPnPError());
	}
}

void bdUPnP::readQueryReply(bdUPnPDevice& currDevice)
{
	const bdInt MAX_QUERY_RESPONSE = 1024;

	if (m_readBuffer == BD_NULL)
	{
		// m_readBufferOffset should be reset when the last read completed
		BD_ASSERT(m_readBufferOffset == 0, "m_readBufferOffset was not reset");

		m_readBuffer = bdAllocate<bdByte8>(MAX_QUERY_RESPONSE + 1);
	}

	// receive on the connected socket
	bdInt bytesReceived = currDevice.m_connection.recv(m_readBuffer + m_readBufferOffset,
		MAX_QUERY_RESPONSE - m_readBufferOffset);

	if (bytesReceived > 0)
	{
		// null terminate the buffer so we can use string search functions
		m_readBuffer[m_readBufferOffset + bytesReceived] = BD_NULL;

		// advance position in buffer to account for new data received
		m_readBufferOffset += bytesReceived;
	}
	else if (bytesReceived == BD_NET_CONNECTION_CLOSED)
	{
		currDevice.m_connection.close();
		currDevice.m_state = bdUPnPDevice::BD_READY;

		//BD_INFO(BD_LOG_LEVEL, "\nUPnP query reply:\n%s\n", m_readBuffer);

		// if no collisions have yet been found (m_UPnPState isn't BD_COLLISION_FOUND)
		// check the query response
		if (m_UPnPState == BD_QUERYING)
		{
			bdByte8 *mappingExists = bdStrstr(m_readBuffer, "200 OK");
	
			// existing port mapping detected
			if (mappingExists)
			{
				// parse the internal address associated with the existing mapping
				const bdByte8 internalClientString[] = "<NewInternalClient";

				bdByte8 *addrLoc = bdStrstr(mappingExists, internalClientString);
				if (!addrLoc)
				{ // query response is improperly formatted, bail out
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}

				// bypass any junk before close of the tag
				addrLoc = bdStrstr(addrLoc, ">");
				if (addrLoc == BD_NULL)
				{ // reply is improperly formatted, bail out
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}
				addrLoc++;

				bdByte8 *addrEnd = bdStrstr(addrLoc, "</"); // find the closing tag

				if (!addrEnd)
				{ // query response is improperly formatted, bail out
					m_UPnPState = BD_FAILED;
					m_errorCode = BD_NET_ERROR;
					BD_FIRE_LISTENER(onUPnPError());
					return;
				}

				bdByte8 addrString[MAX_ADDR_STRING];
				bdUInt addrStringLength = bdUInt(addrEnd - addrLoc);
				bdStrlcpy(addrString, addrLoc, addrStringLength + 1);
				addrString[addrStringLength] = 0;

				// convert string to address to compare with known local addresses
				bdInetAddr mappingAddr(addrString);

				bdBool mappingIsMine = false;

				BD_ASSERT(m_localAddrs, "local addresses not properly assigned in bdUPnP");

				for (bdUInt control = 0; control < m_localAddrs->getSize() && !mappingIsMine; control++) 
				{
					bdInetAddr currAddress;
					m_localAddrs->get(control, currAddress);
					
					if (currAddress == mappingAddr)
					{
						mappingIsMine = true;
					}
				}

				if (mappingIsMine)
				{
					bdNChar8 addressBuffer[BD_ADDR_STRING_SIZE];
					currDevice.m_connectionAddr.toString(addressBuffer , BD_ADDR_STRING_SIZE);
					BD_INFO(BD_LOG_LEVEL, "Usable Port Mapping found on device: %s", addressBuffer);
					currDevice.m_state = bdUPnPDevice::BD_MAPPED;

					// transition straight to mapped state if all other devices are marked as mapped
					bdBool allMappingComplete = true;

					bdLinkedList<bdUPnPDevice>::Position devicePos = m_deviceList.getHeadPosition();

					while (devicePos != BD_NULL && allMappingComplete)
					{
						bdUPnPDevice& queryCheck = m_deviceList.forward(devicePos);
						if (queryCheck.m_state != bdUPnPDevice::BD_MAPPED)
						{
							allMappingComplete = false;
						}
					}

					// all detected UPnP devices are successfully mapped
					if (allMappingComplete)
					{
						// note the existing port mapping
						m_externalPortMap.addTail(lastPortQueried);

						m_timer.reset();
						m_UPnPState = BD_MAPPED;
						BD_FIRE_LISTENER(onMapResponse(true));
						BD_INFO(BD_LOG_LEVEL, "All detected UPnP devices have been successfully mapped");
					}
				}
				else // existing mapping at the current port belongs to someone else
				{
					m_timer.reset();
					m_UPnPState = BD_COLLISION_FOUND;
					BD_FIRE_LISTENER(onQueryResponse(true));
				}
			}
			else
			{
				bdNChar8 addressBuffer[BD_ADDR_STRING_SIZE];
				currDevice.m_connectionAddr.toString(addressBuffer , BD_ADDR_STRING_SIZE);
				BD_INFO(BD_LOG_LEVEL , "No mapping found on device %s" , addressBuffer);
				// transition to the next state if this was the last device being queried
				bdBool queryStillActive = false;

				bdLinkedList<bdUPnPDevice>::Position devicePos = m_deviceList.getHeadPosition();

				while (devicePos != BD_NULL && !queryStillActive)
				{
					bdUPnPDevice& queryCheck = m_deviceList.forward(devicePos);
					if (queryCheck.m_state == bdUPnPDevice::BD_PORT_QUERYING)
					{
						queryStillActive = true;
					}
				}

				// no devices have port collisions
				if (!queryStillActive)
				{
					m_timer.reset();
					m_UPnPState = BD_READY;
					BD_FIRE_LISTENER(onQueryResponse(false));
				}
			}
		}
		// all useful info has been gleaned from read buffer; destroy it
		bdDeallocate<bdByte8>(m_readBuffer);
		m_readBuffer = BD_NULL;
		m_readBufferOffset = 0;
	}
	else if (bytesReceived != BD_NET_WOULD_BLOCK) // socket error on TCP receive
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		m_errorCode = bdSocketStatusCode(bytesReceived);
		BD_FIRE_LISTENER(onUPnPError());
	}
}

void bdUPnP::readMapReply(bdUPnPDevice& currDevice, bdBool unMapping)
{
	if (unMapping)
	{
		BD_ASSERT(m_UPnPState == BD_CLOSING, 
				  "unmap replies should only be fetched in BD_CLOSING state");
	}
	else
	{
		BD_ASSERT(m_UPnPState == BD_OPENING, 
				  "map replies should only be fetched in BD_OPENING state");
	}
	
	// make sure a buffer is allocated for this device
	const bdUInt MAP_BUFF_SIZE = 2048;
	if (m_readBuffer == BD_NULL)
	{
		// m_readBufferOffset should be reset when the last read completed
		BD_ASSERT(m_readBufferOffset == 0, "m_readBufferOffset was not reset");

		m_readBuffer = bdAllocate<bdByte8>(MAP_BUFF_SIZE + 1);
	}

	// receive on the connected socket
	bdInt bytesReceived = currDevice.m_connection.recv(m_readBuffer + m_readBufferOffset,
		MAP_BUFF_SIZE - m_readBufferOffset);

	if (bytesReceived > 0)
	{
		// null terminate the buffer so we can use string search functions
		m_readBuffer[m_readBufferOffset + bytesReceived] = BD_NULL;

		// advance position in buffer to account for new data received
		m_readBufferOffset += bytesReceived;
	}
	else if (bytesReceived == BD_NET_CONNECTION_CLOSED)
	{
		currDevice.m_connection.close();

		//BD_INFO(BD_LOG_LEVEL, "\nUPnP mapping reply:\n%s\n", m_readBuffer);

		// parse for http return code "200 OK"
		bdByte8* succeeded = bdStrstr(m_readBuffer, "200 OK");

		if (succeeded != BD_NULL)
		{
			// port mapping/unmapping completed successfully
			currDevice.m_state = (unMapping ? bdUPnPDevice::BD_UNMAPPED : bdUPnPDevice::BD_MAPPED);

			// transition to the next state if this was the last device mapping/unmapping
			bdBool stillMapping = false;
			
			bdLinkedList<bdUPnPDevice>::Position devicePos = m_deviceList.getHeadPosition();

			while (devicePos != BD_NULL && !stillMapping)
			{
				bdUPnPDevice& mapCheck = m_deviceList.forward(devicePos);
				if (unMapping && mapCheck.m_state == bdUPnPDevice::BD_UNMAPPING)
				{
					stillMapping = true;
				}
				else if (!unMapping && mapCheck.m_state == bdUPnPDevice::BD_MAPPING)
				{
					stillMapping = true;
				}
			}

			if (!stillMapping)
			{
				if (unMapping)
				{
					BD_INFO("info", "UPnP unmapping successful");

					// done with the current iteration of unmapping ports
					// check if there are more port mappings to undo
					if (m_externalPortMap.isEmpty())
					{
						m_timer.reset();
						m_UPnPState = BD_READY;
						BD_FIRE_LISTENER(onMapResponse(true));
					}
					else
					{
						removeNextPortMap();
					}
				}
				else
				{
					// done with port mapping transition to mapped state
					m_timer.reset();
					m_UPnPState = BD_MAPPED;
					BD_INFO("info", "UPnP mapping successful");
					BD_FIRE_LISTENER(onMapResponse(true));
				}
			}
		}
		else
		{
			// port mapping has failed
			m_timer.reset();
			m_UPnPState = BD_FAILED;
			BD_FIRE_LISTENER(onMapResponse(false));
		}

		// all useful info has been gleaned from read buffer; destroy it
		bdDeallocate<bdByte8>(m_readBuffer);
		m_readBuffer = BD_NULL;
		m_readBufferOffset = 0;
	}
	else if (bytesReceived != BD_NET_WOULD_BLOCK) // socket error on TCP receive
	{
		m_timer.reset();
		m_UPnPState = BD_FAILED;
		m_errorCode = bdSocketStatusCode(bytesReceived);
		BD_FIRE_LISTENER(onUPnPError());
	}
}

bdByte8* bdUPnP::genUPnPCommand(const bdUPnPDevice& device, const bdByte8* command, 
									const bdByte8* arguments) const
{
	// generate soap message before the remainder of the http post so that
	// we can determine the content length parameter
	const bdByte8 soapMessage[] =
		"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
		"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"
		"<s:Body>\r\n"
		"<u:%s xmlns:u=\"urn:schemas-upnp-org:service:%s\">\r\n"
		"%s"
		"</u:%s>\r\n"
		"</s:Body>\r\n"
		"</s:Envelope>\r\n";

	const bdUInt maxSoapSize = bdUInt(bdStrlen(soapMessage) + bdStrlen(arguments)
							  + bdStrlen(PPPService) + (2* bdStrlen(command)));

	bdByte8* soapCommand = bdAllocate<bdByte8>(maxSoapSize + 1);

	bdInt actualSoapSize = bdSnprintf(soapCommand, maxSoapSize, soapMessage,
						   command, (device.m_isIp ? IPService : PPPService), arguments, command);

	soapCommand[actualSoapSize] = 0; // null terminate soap string

	const bdByte8 httpPost[] =
		"POST %s HTTP/1.1\r\n"
		"HOST: %s\r\n"
		"SOAPACTION: \"urn:schemas-upnp-org:service:%s#%s\"\r\n"
		"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n"
		"CONTENT-LENGTH: %d\r\n\r\n"
		"%s";

	bdByte8 addressBuff[MAX_ADDR_STRING];
	device.m_connectionAddr.toString(addressBuff, MAX_ADDR_STRING);
	const bdUInt returnSize = bdUInt(bdStrlen(httpPost) + bdStrlen(device.m_controlURL) + 
		bdStrlen(addressBuff) + bdStrlen(PPPService) + 
		bdStrlen(command) + actualSoapSize + 16);
		                  
	bdByte8* returnString = bdAllocate<bdByte8>(returnSize);

	bdSnprintf(returnString, returnSize, httpPost, device.m_controlURL, addressBuff,
			   (device.m_isIp ? IPService : PPPService), command, actualSoapSize,
			   soapCommand);

	bdDeallocate<bdByte8>(soapCommand);
	
	return returnString;
}

void bdUPnP::removeNextPortMap()
{
	BD_ASSERT(m_UPnPState == BD_CLOSING, "this function is only reachable in BD_CLOSING state");

	if (!m_externalPortMap.isEmpty() && !m_deviceList.isEmpty())
	{
		bdPort& currPort = m_externalPortMap.getHead();

		// remove next port mapping from each registered UPnP device
		bdLinkedList<bdUPnPDevice>::Position devicePos = m_deviceList.getHeadPosition();

		while (devicePos != BD_NULL)
		{
			bdUPnPDevice& currDevice = m_deviceList.forward(devicePos);

			// connect to the TCP control port
			bdStreamSocket& currConnection = currDevice.m_connection;

			if (!currDevice.m_connection.create())
			{
				m_timer.reset();
				m_UPnPState = BD_FAILED;
				m_errorCode = BD_NET_ERROR;
				BD_FIRE_LISTENER(onUPnPError());
				return;
			}
			if (!currDevice.m_connection.setBlocking(false))
			{
				m_timer.reset();
				m_UPnPState = BD_FAILED;
				m_errorCode = BD_NET_ERROR;
				BD_FIRE_LISTENER(onUPnPError());
				return;
			}
			
			bdSocketStatusCode sockStatus = currConnection.connect(currDevice.m_connectionAddr);

			// since the socket will take a few cycles to connect,
			// save the http message for later sending
			if (sockStatus == BD_NET_SUCCESS || sockStatus == BD_NET_WOULD_BLOCK)
			{
				const bdByte8 removeCommand[] = "DeletePortMapping";
				const bdByte8 removeArgs[] = 
					"<NewRemoteHost></NewRemoteHost>\r\n"
					"<NewExternalPort>%d</NewExternalPort>\r\n"
					"<NewProtocol>UDP</NewProtocol>\r\n";

				// allocate space for the control string and the filled-in port value
				const bdUInt maxArgLength = bdUInt(bdStrlen(removeArgs) + 10);

				bdByte8* deleteMapArguments = bdAllocate<bdByte8>(maxArgLength);

				// fill in the port for the soap arguments
				bdSnprintf(deleteMapArguments, maxArgLength, removeArgs, currPort);

				BD_ASSERT(currDevice.m_httpRequest == BD_NULL, 
					"http request was not destroyed");

				currDevice.m_httpRequest = genUPnPCommand(currDevice, removeCommand, 
					deleteMapArguments);
				//BD_INFO(BD_LOG_LEVEL, "\nUPnP unmapping request sent:\n%s\n", currDevice->m_httpRequest);

				bdDeallocate<bdByte8>(deleteMapArguments);

				currDevice.m_state = bdUPnPDevice::BD_CONNECT_UNMAP;
			}
			else
			{
				// couldn't connect TCP socket
				m_timer.reset();
				m_UPnPState = BD_FAILED;
				m_errorCode = sockStatus;
				BD_FIRE_LISTENER(onUPnPError());
			}

		}
		
		m_externalPortMap.removeHead();
	}
	else
	{
		// if there aren't any devices or ports remaining in the lists; unmapping is complete
		m_timer.reset();
		m_UPnPState = BD_READY;
	}
}

bdUPnP::bdUPnPDevice::bdUPnPDevice() : 
m_state(BD_READY),
m_controlURL(BD_NULL),
m_isIp(true),
m_httpRequest(BD_NULL)
{
}

bdUPnP::bdUPnPDevice::bdUPnPDevice(const bdUPnPDevice& toCopy) : 
m_state(toCopy.m_state),
m_connectionAddr(toCopy.m_connectionAddr),
m_connection(toCopy.m_connection),
m_isIp(toCopy.m_isIp)
#if defined(BD_PLATFORM_WIN32)
, m_localAddr(toCopy.m_localAddr)
#endif
{
	// deep copy strings
	if (toCopy.m_controlURL)
	{
		bdUWord stringLength = bdStrlen(toCopy.m_controlURL);
		m_controlURL = bdAllocate<bdByte8>(stringLength + 1);
		bdStrlcpy(m_controlURL, toCopy.m_controlURL, stringLength + 1);
	}
	else
	{
		m_controlURL = BD_NULL;
	}

	if (toCopy.m_httpRequest)
	{
		bdUWord stringLength = bdStrlen(toCopy.m_httpRequest);
		m_httpRequest = bdAllocate<bdByte8>(stringLength + 1);
		bdStrlcpy(m_httpRequest, toCopy.m_httpRequest, stringLength + 1);
	}
	else
	{
		m_httpRequest = BD_NULL;
	}
}

bdUPnP::bdUPnPDevice::~bdUPnPDevice()
{
	// make sure strings don't leak memory
	if (m_controlURL)
	{
		bdDeallocate<bdByte8>(m_controlURL);
		m_controlURL = BD_NULL;
	}
	if (m_httpRequest)
	{
		bdDeallocate<bdByte8>(m_httpRequest);
		m_httpRequest = BD_NULL;
	}
}
