/*
Miranda IM: the free IM client for Microsoft* Windows*
Copyright 2000-2012 Miranda ICQ/IM project, 
all portions of this codebase are copyrighted to the people
listed in contributors.txt.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, 
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/
#include "..\..\core\commonheaders.h"
#include "netlib.h"
static const char search_request_msg[] =
	"M-SEARCH * HTTP/1.1\r\n"
	"HOST: 239.255.255.250:1900\r\n"
	"MAN: \"ssdp:discover\"\r\n"
	"MX: 1\r\n"
	"ST: urn:schemas-upnp-org:service:%s\r\n"
	"\r\n";
static const char xml_get_hdr[] =
	"GET %s HTTP/1.1\r\n"
	"HOST: %s:%u\r\n"
	"ACCEPT-LANGUAGE: *\r\n\r\n";
static const char soap_post_hdr[] =
	"POST %s HTTP/1.1\r\n"
	"HOST: %s:%u\r\n"
	"CONTENT-LENGTH: %u\r\n"
	"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n"
	"SOAPACTION: \"%s#%s\"\r\n\r\n"
	"%s";
static const char soap_post_hdr_m[] =
	"M-POST %s URL HTTP/1.1\r\n"
	"HOST: %s:%u\r\n"
	"CONTENT-LENGTH: %u\r\n"
	"CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n"
	"MAN: \"http://schemas.xmlsoap.org/soap/envelope/\"; ns=01\r\n"
	"01-SOAPACTION: \"%s#%s\"\r\n\r\n"
	"%s";
static const char search_device[] =
	"%s";
static const char soap_action[] =
	"\r\n"
	"\r\n"
	"  \r\n"
	"    \r\n"
	"%s"
	"    \r\n"
	"  \r\n"
	"\r\n";
static const char soap_query[] =
	"\r\n" 
	"  \r\n"
	"    \r\n"
	"      %s\r\n"
	"    \r\n"
	"  \r\n"
	"\r\n";
static const char add_port_mapping[] =
	"      \r\n"
	"      %i\r\n"
	"      %s\r\n"
	"      %i\r\n"
	"      %s\r\n"
	"      1\r\n"
	"      Miranda\r\n"
	"      0\r\n";
static const char delete_port_mapping[] =
	"     \r\n"
	"     %i\r\n"
	"     %s\r\n";
static const char get_port_mapping[] =
	"     %i\r\n";
static bool gatewayFound;
static SOCKADDR_IN locIP;
static time_t lastDiscTime;
static int expireTime = 120;
static int retryCount;
static SOCKET sock = INVALID_SOCKET;
static char szConnHost[256];
static unsigned short sConnPort;
static WORD *portList;
static unsigned numports, numportsAlloc;
static HANDLE portListMutex;
static char szCtlUrl[256], szDev[256];
typedef enum
{
	DeviceGetReq, 
	ControlAction, 
	ControlQuery
} ReqType;
static bool txtParseParam(char* szData, char* presearch, 
	char* start, char* finish, char* param, size_t size)
{
	char *cp, *cp1;
	size_t len;
	*param = 0;
	if (presearch != NULL) {
		cp1 = strstr(szData, presearch);
		if (cp1 == NULL) return false;
	}
	else
		cp1 = szData;
	cp = strstr(cp1, start);
	if (cp == NULL) return false;
	cp += strlen(start);
	while (*cp == ' ') ++cp;
	cp1 = strstr(cp, finish);
	if (cp1 == NULL) return false;
	while (*(cp1-1) == ' ' && cp1 > cp) --cp1;
	len = min((size_t)(cp1 - cp), size-1);
	strncpy(param, cp, len);
	param[len] = 0;
	return true;
}
void parseURL(char* szUrl, char* szHost, unsigned short* sPort, char* szPath)
{
	char *ppath, *phost, *pport;
	int sz;
	phost = strstr(szUrl, "://");
	if (phost == NULL) phost = szUrl;
	else phost += 3;
	ppath = strchr(phost, '/');
	if (ppath == NULL) ppath = phost + strlen(phost);
	pport = strchr(phost, ':');
	if (pport == NULL) pport = ppath;
	if (szHost != NULL)
	{
		sz = pport - phost + 1;
		if (sz>256) sz = 256;
		strncpy(szHost, phost, sz);
		szHost[sz-1] = 0;
	}
	if (sPort != NULL)
	{
		if (pport < ppath)
		{
			long prt = atol(pport+1);
			*sPort = prt != 0 ? (unsigned short)prt : 80;
		}
		else
			*sPort = 80;
	}
	if (szPath != NULL)
	{
		strncpy(szPath, ppath, 256);
		szPath[255] = 0;
	}
}
static void LongLog(char* szData)
{
	CallService(MS_NETLIB_LOG, 0, (LPARAM)szData);
}
static void closeRouterConnection(void)
{
	if (sock != INVALID_SOCKET)
	{
		closesocket(sock);
		sock = INVALID_SOCKET;
	}
}
static void validateSocket(void)
{
	static const TIMEVAL tv = { 0, 0 };
	fd_set rfd;
	char buf[4];
	bool opened;  
	if (sock == INVALID_SOCKET)
		return;
	FD_ZERO(&rfd);
	FD_SET(sock, &rfd);
	switch (select(1, &rfd, NULL, NULL, &tv))
	{
	case SOCKET_ERROR:
		opened = false;
		break;
	case 0:
		opened = true;
		break;
	case 1:
		opened = recv(sock, buf, 1, MSG_PEEK) > 0;
		break;
	}
	if (!opened)
		closeRouterConnection();
}
static int httpTransact(char* szUrl, char* szResult, int resSize, char* szActionName, ReqType reqtype)
{
	// Parse URL
	char szHost[256], szPath[256], szRes[16];
	int sz = 0, res = 0;
	unsigned short sPort;
	bool needClose;
	const char* szPostHdr = soap_post_hdr;
	char* szData = ( char* )mir_alloc(4096);
	char* szReq =  NULL;
	parseURL(szUrl, szHost, &sPort, szPath);
	if (sPort != sConnPort || _stricmp(szHost, szConnHost))
		closeRouterConnection();
	else
		validateSocket();
	for (;;)
	{
		retryCount = 0;
		switch(reqtype) 
		{
		case DeviceGetReq:
			sz = mir_snprintf (szData, 4096, xml_get_hdr, szPath, szHost, sPort);
			break;
		case ControlAction:
			{
				char szData1[1024];
				szReq = mir_strdup(szResult);
				sz = mir_snprintf (szData1, sizeof(szData1), 
					soap_action, szActionName, szDev, szReq, szActionName);
				sz = mir_snprintf (szData, 4096, 
					szPostHdr, szPath, szHost, sPort, 
					sz, szDev, szActionName, szData1);
			}
			break;
		case ControlQuery:
			{
				char szData1[1024];
				sz = mir_snprintf (szData1, sizeof(szData1), 
					soap_query, szActionName);
				sz = mir_snprintf (szData, 4096, 
					szPostHdr, szPath, szHost, sPort, 
					sz, "urn:schemas-upnp-org:control-1-0", "QueryStateVariable", szData1);
			}
			break;
		}
		szResult[0] = 0;
		{
			static const TIMEVAL tv = { 6, 0 };
			static unsigned ttl = 4;
			static u_long mode = 1;
			fd_set rfd, wfd, efd;
			SOCKADDR_IN enetaddr;
retrycon:
			if (sock == INVALID_SOCKET)
			{
				sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
				enetaddr.sin_family = AF_INET;
				enetaddr.sin_port = htons(sPort);
				enetaddr.sin_addr.s_addr = inet_addr(szHost);
				// Resolve host name if needed
				if (enetaddr.sin_addr.s_addr == INADDR_NONE)
				{
					PHOSTENT he = gethostbyname(szHost);
					if (he)
						enetaddr.sin_addr.s_addr = *(unsigned*)he->h_addr_list[0];
				}
				NetlibLogf(NULL, "UPnP HTTP connection Host: %s Port: %u", szHost, sPort);
				FD_ZERO(&rfd); FD_ZERO(&wfd); FD_ZERO(&efd);
				FD_SET(sock, &rfd); FD_SET(sock, &wfd); FD_SET(sock, &efd);
				// Limit the scope of the connection (does not work for
				setsockopt(sock, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(unsigned));
				// Put socket into non-blocking mode for timeout on connect
				ioctlsocket(sock, FIONBIO, &mode);
				// Connect to the remote host
				if (connect(sock, (SOCKADDR*)&enetaddr, sizeof(enetaddr)) == SOCKET_ERROR)
				{
					int err = WSAGetLastError();
					// Socket connection failed
					if (err != WSAEWOULDBLOCK)
					{
						closeRouterConnection();
						NetlibLogf(NULL, "UPnP connect failed %d", err);
						break;
					}
					// Wait for socket to connect
					else if (select(1, &rfd, &wfd, &efd, &tv) != 1)
					{
						closeRouterConnection();
						NetlibLogf(NULL, "UPnP connect timeout");
						break;
					}
					else if (!FD_ISSET(sock, &wfd))
					{
						closeRouterConnection();
						NetlibLogf(NULL, "UPnP connect failed");
						break;
					}
				}
				strcpy(szConnHost, szHost); sConnPort = sPort;
			}
			if (send(sock, szData, sz, 0) != SOCKET_ERROR)
			{
				char *hdrend = NULL;
				int acksz = 0, pktsz = 0;
				if (szActionName == NULL)
				{
					int len = sizeof(locIP);
					getsockname(sock, (SOCKADDR*)&locIP, &len);
					if (locIP.sin_addr.S_un.S_addr == 0x0100007f)
					{
						struct hostent *he;
						gethostname(szPath, sizeof(szPath));
						he = gethostbyname(szPath);
						if (he != NULL)
							locIP.sin_addr.S_un.S_addr = *(PDWORD)he->h_addr_list[0];
					}
				}
				LongLog(szData);
				sz = 0;
				for (;;)
				{
					int bytesRecv;
					FD_ZERO(&rfd);
					FD_SET(sock, &rfd);
					// Wait for the next packet
					if (select(1, &rfd, NULL, NULL, &tv) != 1)
					{
						closeRouterConnection();
						NetlibLogf(NULL, "UPnP recieve timeout");
						break;
					}
					//
					bytesRecv = recv(sock, &szResult[sz], resSize-sz, 0);
					// Connection closed or aborted, all data received
					if (bytesRecv == 0 || bytesRecv == SOCKET_ERROR)
					{
						closeRouterConnection();
						if ((bytesRecv == SOCKET_ERROR || sz == 0) && retryCount < 2)
						{
							++retryCount;
							goto retrycon;
						}
						break;
					}
					sz += bytesRecv;
					// Insert null terminator to use string functions
					if (sz >= (resSize-1))
					{
						szResult[resSize-1] = 0;
						break;
					}
					else
						szResult[sz] = 0;
					// HTTP header found?
					if (hdrend == NULL)
					{
						// Find HTTP header end
						hdrend = strstr(szResult, "\r\n\r\n");
						if (hdrend == NULL)
						{
							hdrend = strstr(szResult, "\n\n");
							if (hdrend) hdrend += 2;
						}
						else
							hdrend += 4;
						if (hdrend != NULL)
						{
							// Get packet size if provided
							if (txtParseParam(szResult, NULL, "Content-Length:", "\n", szRes, sizeof(szRes)) ||
								txtParseParam(szResult, NULL, "CONTENT-LENGTH:", "\n", szRes, sizeof(szRes)))
							{
								// Add size of HTTP header to the packet size to compute full transmission size
								pktsz = atol(ltrimp(szRes)) + (hdrend - szResult);
							}
							// Get encoding type if provided
							else if (txtParseParam(szResult, NULL, "Transfer-Encoding:", "\n", szRes, sizeof(szRes)))
							{
								if (_stricmp(lrtrimp(szRes), "Chunked") == 0)
									acksz = hdrend - szResult;
							}
							if (txtParseParam(szResult, NULL, "Connection:", "\n", szRes, sizeof(szRes)))
							{
								needClose = (_stricmp(lrtrimp(szRes), "close") == 0);
							}
						}
					}
					// Content-Length bytes reached, all data received
					if (sz >= pktsz && pktsz != 0)
					{
						szResult[pktsz] = 0;
						break;
					}
					// Chunked encoding processing
					if (sz > acksz && acksz != 0)
					{
retry:
						// Parse out chunk size
						char* data = szResult + acksz;
						char* peol1 = data == hdrend ? data - 1 : strchr(data, '\n');
						if (peol1 != NULL)
						{
							char *peol2 = strchr(++peol1, '\n');
							if (peol2 != NULL)
							{
								// Get chunk size
								int chunkBytes = strtol(peol1, NULL, 16);
								acksz += chunkBytes;
								peol2++;
								memmove(data, peol2, strlen(peol2) + 1);
								sz -= peol2 - data;
								// Last chunk, all data received
								if (chunkBytes == 0) break;
								if (sz > acksz) goto retry;
							}
						}
					}
				}
				LongLog(szResult);
			}
			else
			{
				if (retryCount < 2)
				{
					closeRouterConnection();
					++retryCount;
					goto retrycon;
				}
				else
					NetlibLogf(NULL, "UPnP send failed %d", WSAGetLastError());
			}
		}
		txtParseParam(szResult, "HTTP", " ", " ", szRes, sizeof(szRes));
		res = atol(szRes);
		if (szActionName != NULL && res == 405 && szPostHdr == soap_post_hdr)
			szPostHdr = soap_post_hdr_m;
		else
			break;
	}
	if (needClose)
		closeRouterConnection();
	mir_free(szData);
	mir_free(szReq);
	return res;
}
static unsigned getExtIP(void)
{
	char szExtIP[30];
	char* szData = (char*)mir_alloc(4096); szData[0] = 0;
	unsigned extip = 0;
	int res = httpTransact(szCtlUrl, szData, 4096, "GetExternalIPAddress", ControlAction);
	if (res == 200 && txtParseParam(szData, "", "<", szExtIP, sizeof(szExtIP)))
		extip = ntohl(inet_addr(szExtIP));
	mir_free(szData);
	return extip;
}
static bool getUPnPURLs(char* szUrl, size_t sizeUrl)
{
	char* szData = (char*)mir_alloc(8192);
	gatewayFound = httpTransact(szUrl, szData, 8192, NULL, DeviceGetReq) == 200;
	if (gatewayFound)
	{
		char szTemp[256], *rpth;
		size_t ctlLen;
		txtParseParam(szData, NULL, "", "", szTemp, sizeof(szTemp));
		strncpy(szCtlUrl, szTemp[0] ? szTemp : szUrl, sizeof(szCtlUrl));
		szCtlUrl[sizeof(szCtlUrl)-1] = 0;
		mir_snprintf(szTemp, sizeof(szTemp), search_device, szDev);
		txtParseParam(szData, szTemp, "", "", szUrl, sizeUrl);
		// URL combining per RFC 2396
		if ( szUrl[0] != 0 )
		{
			if (strstr(szUrl, "://") != NULL)                     // absolute URI
				rpth = szCtlUrl;
			else if (strncmp(szUrl, "//", 2) == 0)                // relative URI net_path
			{
				rpth = strstr(szCtlUrl, "//");
				if (rpth == NULL) rpth = szCtlUrl;
			}
			else if (szUrl[0] == '/')                             // relative URI abs_path
			{
				rpth = strstr(szCtlUrl, "//");
				rpth = rpth ? rpth + 2 : szCtlUrl;
				rpth = strchr(rpth, '/');
				if (rpth == NULL) rpth = szCtlUrl + strlen(szCtlUrl);
			}
			else
			{                                                      // relative URI rel_path
				size_t ctlCLen = strlen(szCtlUrl);
				rpth = szCtlUrl + ctlCLen;
				if (ctlCLen != 0 && *(rpth-1) != '/')
					strncpy(rpth++, "/", sizeof(szCtlUrl) - ctlCLen);
			}
			ctlLen = sizeof(szCtlUrl) - (rpth - szCtlUrl);
			strncpy(rpth, szUrl, ctlLen);
			szCtlUrl[sizeof(szCtlUrl)-1] = 0;
		}
		else
		{
			szCtlUrl[0] = 0;
			gatewayFound = false;
		}
	}
	mir_free(szData);
	return gatewayFound;
}
static void discoverUPnP(void)
{
	char* buf;
	int buflen;
	unsigned i, j, nip = 0;
	unsigned* ips = NULL;
	static const unsigned any = INADDR_ANY;
	static const TIMEVAL tv = { 1, 600000 };
	char szUrl[256] = "";
	char hostname[256];
	PHOSTENT he;
	fd_set readfd;
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	SOCKADDR_IN enetaddr;
	enetaddr.sin_family = AF_INET;
	enetaddr.sin_port = htons(1900);
	enetaddr.sin_addr.s_addr = inet_addr("239.255.255.250");
	gethostname(hostname, sizeof(hostname));
	he = gethostbyname(hostname);
	if (he)
	{
		while (he->h_addr_list[nip]) ++nip;
		ips = ( unsigned* )mir_alloc(nip * sizeof(unsigned));
		for (j = 0; j < nip; ++j)
			ips[j] = *(unsigned*)he->h_addr_list[j];
	}
	buf = (char*)mir_alloc(1500);
	for (i = 3;  --i && szUrl[0] == 0;)
	{
		for (j = 0; j < nip; ++j)
		{
			if (ips)
				setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (char *)&ips[j], sizeof(unsigned));
			buflen = mir_snprintf(buf, 1500, search_request_msg, "WANIPConnection:1");
			sendto(sock, buf, buflen, 0, (SOCKADDR*)&enetaddr, sizeof(enetaddr));
			LongLog(buf);
			buflen = mir_snprintf(buf, 1500, search_request_msg, "WANPPPConnection:1");
			sendto(sock, buf, buflen, 0, (SOCKADDR*)&enetaddr, sizeof(enetaddr));
			LongLog(buf);
		}
		if (Miranda_Terminated()) break;
		FD_ZERO(&readfd);
		FD_SET(sock, &readfd);
		while (select(1, &readfd, NULL, NULL, &tv) >= 1)
		{
			buflen = recv(sock, buf, 1500, 0);
			if (buflen != SOCKET_ERROR)
			{
				buf[buflen] = 0;
				LongLog(buf);
				if (txtParseParam(buf, NULL, "LOCATION:", "\n", szUrl, sizeof(szUrl)) ||
					txtParseParam(buf, NULL, "Location:", "\n", szUrl, sizeof(szUrl)))
				{
					char age[30];
					char szHostNew[256], szHostExist[256];
					lrtrim(szUrl);
					parseURL(szUrl, szHostNew, NULL, NULL);
					parseURL(szCtlUrl, szHostExist, NULL, NULL);
					if (strcmp(szHostNew, szHostExist) == 0)
					{
						gatewayFound = true;
						break;
					}
					txtParseParam(buf, NULL, "ST:", "\n", szDev, sizeof(szDev));
					txtParseParam(buf, "max-age", "=", "\n", age, sizeof(age));
					expireTime = atoi(lrtrimp(age));
					lrtrim(szDev);
					if (getUPnPURLs(szUrl, sizeof(szUrl)))
					{
						gatewayFound = getExtIP() != 0;
						if (gatewayFound) break;
					}
				}
			}
			FD_ZERO(&readfd);
			FD_SET(sock, &readfd);
		}
	}
	mir_free(buf);
	mir_free(ips);
	setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (char *)&any, sizeof(unsigned));
	closesocket(sock);
}
static bool findUPnPGateway(void)
{
	if ((time(NULL) - lastDiscTime) >= expireTime)
	{
		WaitForSingleObject(portListMutex, INFINITE);
		time_t curTime = time(NULL);
		if ((curTime - lastDiscTime) >= expireTime)
		{
			gatewayFound = false;
			discoverUPnP();
			lastDiscTime = curTime;
			NetlibLogf(NULL, "UPnP Gateway detected %d, Control URL: %s", gatewayFound, szCtlUrl);
		}
		ReleaseMutex(portListMutex);
	}
	return gatewayFound;
}
bool NetlibUPnPAddPortMapping(WORD intport, char *proto, WORD *extport, DWORD *extip, bool search)
{
	int res = 0, i = 5;
	if (findUPnPGateway())
	{
		char* szData = (char*)mir_alloc(4096);
		char szExtIP[30];
		*extport = intport - 1;
		*extip = ntohl(locIP.sin_addr.S_un.S_addr);
		WaitForSingleObject(portListMutex, INFINITE);
		do
		{
			++*extport;
			mir_snprintf(szData, 4096, add_port_mapping, 
				*extport, proto, intport, inet_ntoa(locIP.sin_addr));
			res = httpTransact(szCtlUrl, szData, 4096, "AddPortMapping", ControlAction);
			txtParseParam(szData, NULL, "", "", szExtIP, sizeof(szExtIP));
		}
		while (search && res == 500 && atol(szExtIP) == 718 && --i);
		mir_free(szData);
		if (res == 200)
		{
			unsigned ip = getExtIP();
			if (ip) *extip = ip; 
			if (numports >= numportsAlloc)
				mir_realloc(portList, sizeof(WORD)*(numportsAlloc += 10));
			portList[numports++] = *extport;
		}
		ReleaseMutex(portListMutex);
	}
	return res == 200;
}
void NetlibUPnPDeletePortMapping(WORD extport, char* proto)
{
	if (extport == 0)
		return;
	//	findUPnPGateway();
	if (gatewayFound)
	{
		unsigned i;
		char* szData = (char*)mir_alloc(4096);
		WaitForSingleObject(portListMutex, INFINITE);
		mir_snprintf(szData, 4096, delete_port_mapping, extport, proto);
		httpTransact(szCtlUrl, szData, 4096, "DeletePortMapping", ControlAction);
		for (i = 0; i < numports; ++i)
			if (portList[i] == extport && --numports > 0)
				memmove(&portList[i], &portList[i+1], (numports - i) * sizeof(WORD));
		mir_free(szData);
		ReleaseMutex(portListMutex);
	}
}
void NetlibUPnPCleanup(void*)
{
	if (DBGetContactSettingByte(NULL, "Netlib", "NLEnableUPnP", 1) == 0)
		// upnp is disabled globally, no need for a cleanup
		return;
	{
		int i, incoming = 0;
		EnterCriticalSection(&csNetlibUser);
		for (i = 0; i < netlibUser.getCount(); ++i)
		{
			if (netlibUser[i]->user.flags & NUF_INCOMING)
			{
				incoming = 1;
				break;
			}
		}
		LeaveCriticalSection(&csNetlibUser);
		if (!incoming) return;
	}
	if (findUPnPGateway())
	{
		char* szData = (char*)alloca(4096);
		char buf[50], lip[50];
		unsigned i, j = 0, k, num = 100;
		WORD ports[30];
		strcpy(lip, inet_ntoa(locIP.sin_addr));
		WaitForSingleObject(portListMutex, INFINITE);
		if (httpTransact(szCtlUrl, szData, 4096, "PortMappingNumberOfEntries", ControlQuery) == 200 &&
			txtParseParam(szData, "QueryStateVariableResponse", "", "<", buf, sizeof(buf)))
			num = atol(buf);
		for (i=0; i", "<", buf, sizeof(buf)) || strcmp(buf, "Miranda") != 0)
				continue;
			if (!txtParseParam(szData, "", "<", buf, sizeof(buf)) || strcmp(buf, lip) != 0)
				continue;
			if (txtParseParam(szData, "", "<", buf, sizeof(buf)))
			{
				WORD mport = (WORD)atol(buf);
				if (j >= SIZEOF(ports)) 
					break;
				for (k=0; k= numports)
					ports[j++] = mport;
			}
		}
		ReleaseMutex(portListMutex);
		for (i=0; i