/*
Plugin of Miranda IM for communicating with users of the MSN Messenger protocol.
Copyright (c) 2007-2011 Boris Krasnovskiy.

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, see <http://www.gnu.org/licenses/>.
*/

#include "msn_global.h"
#include "msn_proto.h"
#include "SDK/netfw.h"

#ifndef CLSID_NetFwMgr
#define MDEF_CLSID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
	const CLSID name = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }

	MDEF_CLSID(CLSID_NetFwMgr, 0x304ce942, 0x6e39, 0x40d8, 0x94, 0x3a, 0xb9, 0x13, 0xc4, 0x0c, 0x9c, 0xd4);
	MDEF_CLSID(IID_INetFwMgr, 0xf7898af5, 0xcac4, 0x4632, 0xa2, 0xec, 0xda ,0x06, 0xe5, 0x11, 0x1a, 0xf2);
#endif


MyConnectionType MyConnection;

const char* conStr[] =
{
	"Unknown-Connect",
	"Direct-Connect",
	"Unknown-NAT",
	"IP-Restrict-NAT",
	"Port-Restrict-NAT",
	"Symmetric-NAT",
	"Firewall",
	"ISALike"
};


void CMsnProto::DecryptEchoPacket(UDPProbePkt& pkt)
{
	pkt.clientPort ^= 0x3141;
	pkt.discardPort ^= 0x3141;
	pkt.testPort ^= 0x3141;
	pkt.clientIP ^= 0x31413141;
	pkt.testIP ^= 0x31413141;


	IN_ADDR addr;
	MSN_DebugLog("Echo packet: version: 0x%x  service code: 0x%x  transaction ID: 0x%x",
		pkt.version, pkt.serviceCode, pkt.trId);
	addr.S_un.S_addr = pkt.clientIP;
	MSN_DebugLog("Echo packet: client port: %u  client addr: %s",
		pkt.clientPort, inet_ntoa(addr));
	addr.S_un.S_addr = pkt.testIP;
	MSN_DebugLog("Echo packet: discard port: %u  test port: %u test addr: %s",
		pkt.discardPort, pkt.testPort, inet_ntoa(addr));
}


static void DiscardExtraPackets(SOCKET s)
{
	Sleep(3000);

	static const TIMEVAL tv = {0, 0};
	unsigned buf;

	for (;;)
	{
		if (Miranda_Terminated()) break;

		fd_set fd;
		FD_ZERO(&fd);
		FD_SET(s, &fd);

		if (select(1, &fd, NULL, NULL, &tv) == 1)
			recv(s, (char*)&buf, sizeof(buf), 0);
		else
			break;
	}
}


void CMsnProto::MSNatDetect(void)
{
	unsigned i;

	PHOSTENT host = gethostbyname("echo.edge.messenger.live.com");
	if (host == NULL)
	{
		MSN_DebugLog("P2PNAT could not find echo server \"echo.edge.messenger.live.com\"");
		return;
	}

	SOCKADDR_IN addr;
	addr.sin_family = AF_INET;
	addr.sin_port = _htons(7001);
	addr.sin_addr = *( PIN_ADDR )host->h_addr_list[0];

	MSN_DebugLog("P2PNAT Detected echo server IP %d.%d.%d.%d",
		addr.sin_addr.S_un.S_un_b.s_b1, addr.sin_addr.S_un.S_un_b.s_b2,
		addr.sin_addr.S_un.S_un_b.s_b3, addr.sin_addr.S_un.S_un_b.s_b4);

	SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	connect(s, (SOCKADDR*)&addr, sizeof(addr));

	UDPProbePkt pkt = { 0 };
	UDPProbePkt pkt2;

	// Detect My IP
	pkt.version = 2;
	pkt.serviceCode = 4;
	send(s, (char*)&pkt, sizeof(pkt), 0);

	SOCKADDR_IN  myaddr;
	int szname = sizeof(myaddr);
	getsockname(s, (SOCKADDR*)&myaddr, &szname);

	MyConnection.intIP = myaddr.sin_addr.S_un.S_addr;
	MSN_DebugLog("P2PNAT Detected IP facing internet %d.%d.%d.%d",
		myaddr.sin_addr.S_un.S_un_b.s_b1, myaddr.sin_addr.S_un.S_un_b.s_b2,
		myaddr.sin_addr.S_un.S_un_b.s_b3, myaddr.sin_addr.S_un.S_un_b.s_b4);

	SOCKET s1 = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	pkt.version = 2;
	pkt.serviceCode = 1;
	pkt.clientPort = 0x3141;
	pkt.clientIP = 0x31413141;

	UDPProbePkt rpkt = {0};

	// NAT detection
	for (i=0; i<4; ++i)
	{
		if (Miranda_Terminated()) break;

		// Send echo request to server 1
		MSN_DebugLog("P2PNAT Request 1 attempt %d sent", i);
		sendto(s1, (char*)&pkt, sizeof(pkt), 0, (SOCKADDR*)&addr, sizeof(addr));

		fd_set fd;
		FD_ZERO(&fd);
		FD_SET(s1, &fd);
		TIMEVAL tv = {0, 200000 * (1 << i) };

		if (select(1, &fd, NULL, NULL, &tv) == 1)
		{
			MSN_DebugLog("P2PNAT Request 1 attempt %d response", i);
			recv(s1, (char*)&rpkt, sizeof(rpkt), 0);
			pkt2 = rpkt;
			DecryptEchoPacket(rpkt);
			break;
		}
		else
			MSN_DebugLog("P2PNAT Request 1 attempt %d timeout", i);
	}

	closesocket(s);

	// Server did not respond
	if (i >= 4)
	{
		MyConnection.udpConType = conFirewall;
		closesocket(s1);
		return;
	}

	MyConnection.extIP = rpkt.clientIP;

	// Check if NAT not found
	if (MyConnection.extIP == MyConnection.intIP)
	{
		if (msnExternalIP != NULL && inet_addr(msnExternalIP) != MyConnection.extIP)
			MyConnection.udpConType = conISALike;
		else
			MyConnection.udpConType = conDirect;

		closesocket(s1);
		return;
	}

	// Detect UPnP NAT
	NETLIBBIND nlb = {0};
	nlb.cbSize = sizeof(nlb);
	nlb.pfnNewConnectionV2 = MSN_ConnectionProc;
	nlb.pExtra = this;

	HANDLE sb = (HANDLE) MSN_CallService(MS_NETLIB_BINDPORT, (WPARAM)hNetlibUser, (LPARAM)&nlb);
	if ( sb != NULL )
	{
		MyConnection.upnpNAT = htonl(nlb.dwExternalIP) == MyConnection.extIP;
		Sleep(100);
		Netlib_CloseHandle(sb);
	}

	DiscardExtraPackets(s1);

	// Start IP Restricted NAT detection
	UDPProbePkt rpkt2 = {0};
	pkt2.serviceCode = 3;
	SOCKADDR_IN addr2 = addr;
	addr2.sin_addr.S_un.S_addr = rpkt.testIP;
	addr2.sin_port = rpkt.discardPort;
	for (i=0; i<4; ++i)
	{
		if (Miranda_Terminated()) break;

		MSN_DebugLog("P2PNAT Request 2 attempt %d sent", i);
		// Remove IP restriction for server 2
		sendto(s1, NULL, 0, 0, (SOCKADDR*)&addr2, sizeof(addr2));
		// Send echo request to server 1 for server 2
		sendto(s1, (char*)&pkt2, sizeof(pkt2), 0, (SOCKADDR*)&addr, sizeof(addr));

		fd_set fd;
		FD_ZERO(&fd);
		FD_SET(s1, &fd);
		TIMEVAL tv = {0, 200000 * (1 << i) };

		if (select(1, &fd, NULL, NULL, &tv) == 1)
		{
			MSN_DebugLog("P2PNAT Request 2 attempt %d response", i);
			recv(s1, (char*)&rpkt2, sizeof(rpkt2), 0);
			DecryptEchoPacket(rpkt2);
			break;
		}
		else
			MSN_DebugLog("P2PNAT Request 2 attempt %d timeout", i);
	}

	// Response recieved so it's an IP Restricted NAT (Restricted Cone NAT)
	// (MSN does not detect Full Cone NAT and consider it as IP Restricted NAT)
	if (i < 4)
	{
		MyConnection.udpConType = conIPRestrictNAT;
		closesocket(s1);
		return;
	}

	DiscardExtraPackets(s1);

	// Symmetric NAT detection
	addr2.sin_port = rpkt.testPort;
	for (i=0; i<4; ++i)
	{
		if (Miranda_Terminated()) break;

		MSN_DebugLog("P2PNAT Request 3 attempt %d sent", i);
		// Send echo request to server 1
		sendto(s1, (char*)&pkt, sizeof(pkt), 0, (SOCKADDR*)&addr2, sizeof(addr2));

		fd_set fd;
		FD_ZERO(&fd);
		FD_SET(s1, &fd);
		TIMEVAL tv = {1 << i, 0 };

		if ( select(1, &fd, NULL, NULL, &tv) == 1 )
		{
			MSN_DebugLog("P2PNAT Request 3 attempt %d response", i);
			recv(s1, (char*)&rpkt2, sizeof(rpkt2), 0);
			DecryptEchoPacket(rpkt2);
			break;
		}
		else
			MSN_DebugLog("P2PNAT Request 3 attempt %d timeout", i);
	}
	if (i < 4)
	{
		// If ports different it's symmetric NAT
		MyConnection.udpConType = rpkt.clientPort == rpkt2.clientPort ?
			conPortRestrictNAT : conSymmetricNAT;
	}
	closesocket(s1);
}


static bool IsIcfEnabled(void)
{
	HRESULT hr;
	VARIANT_BOOL fwEnabled = VARIANT_FALSE;

	INetFwProfile* fwProfile = NULL;
	INetFwMgr* fwMgr = NULL;
	INetFwPolicy* fwPolicy = NULL;
	INetFwAuthorizedApplication* fwApp = NULL;
	INetFwAuthorizedApplications* fwApps = NULL;
	BSTR fwBstrProcessImageFileName = NULL;
	wchar_t *wszFileName = NULL;

	hr = CoInitialize(NULL);
	if (FAILED(hr)) return false;

	// Create an instance of the firewall settings manager.
	hr = CoCreateInstance(CLSID_NetFwMgr, NULL, CLSCTX_INPROC_SERVER,
			IID_INetFwMgr, (void**)&fwMgr );
	if (FAILED(hr)) goto error;

	// Retrieve the local firewall policy.
	hr = fwMgr->get_LocalPolicy(&fwPolicy);
	if (FAILED(hr)) goto error;

	// Retrieve the firewall profile currently in effect.
	hr = fwPolicy->get_CurrentProfile(&fwProfile);
	if (FAILED(hr)) goto error;

	// Get the current state of the firewall.
	hr = fwProfile->get_FirewallEnabled(&fwEnabled);
	if (FAILED(hr)) goto error;

	if (fwEnabled == VARIANT_FALSE) goto error;

	// Retrieve the authorized application collection.
	hr = fwProfile->get_AuthorizedApplications(&fwApps);
	if (FAILED(hr)) goto error;

	TCHAR szFileName[MAX_PATH];
	GetModuleFileName(NULL, szFileName, SIZEOF(szFileName));

	wszFileName = mir_t2u(szFileName);

	// Allocate a BSTR for the process image file name.
	fwBstrProcessImageFileName = SysAllocString(wszFileName);
	if (FAILED(hr)) goto error;

	// Attempt to retrieve the authorized application.
	hr = fwApps->Item(fwBstrProcessImageFileName, &fwApp);
	if (SUCCEEDED(hr))
	{
		// Find out if the authorized application is enabled.
		fwApp->get_Enabled(&fwEnabled);
		fwEnabled = ~fwEnabled;
	}

error:
	// Free the BSTR.
	SysFreeString(fwBstrProcessImageFileName);
	mir_free(wszFileName);

	// Release the authorized application instance.
	if (fwApp != NULL) fwApp->Release();

	// Release the authorized application collection.
	if (fwApps != NULL) fwApps->Release();

	// Release the firewall profile.
	if (fwProfile != NULL) fwProfile->Release();

	// Release the local firewall policy.
	if (fwPolicy != NULL) fwPolicy->Release();

	// Release the firewall settings manager.
	if (fwMgr != NULL) fwMgr->Release();

	CoUninitialize();

	return fwEnabled != VARIANT_FALSE;
}


void CMsnProto::MSNConnDetectThread( void* )
{
	char parBuf[512] = "";

	memset(&MyConnection, 0, sizeof(MyConnection));

	MyConnection.icf = IsIcfEnabled();
	bool portsMapped = getByte("NLSpecifyIncomingPorts", 0) != 0;

	unsigned gethst = getByte("AutoGetHost", 1);
	switch (gethst)
	{
		case 0:
			MSN_DebugLog("P2PNAT User overwrote IP connection is guessed by user settings only");

			// User specified host by himself so check if it matches MSN information
			// if it does, move to connection type autodetection,
			// if it does not, guess connection type from available info
			getStaticString(NULL, "YourHost", parBuf, sizeof(parBuf));
			if (msnExternalIP == NULL || strcmp(msnExternalIP, parBuf) != 0)
			{
				MyConnection.extIP = inet_addr( parBuf );
				if ( MyConnection.extIP == INADDR_NONE )
				{
					PHOSTENT myhost = gethostbyname( parBuf );
					if ( myhost != NULL )
						MyConnection.extIP = ((PIN_ADDR)myhost->h_addr)->S_un.S_addr;
					else
						setByte("AutoGetHost", 1);
				}
				if ( MyConnection.extIP != INADDR_NONE )
				{
					MyConnection.intIP = MyConnection.extIP;
					MyConnection.udpConType = MyConnection.extIP ? (ConEnum)portsMapped : conUnknown;
					MyConnection.CalculateWeight();
					return;
				}
				else
					MyConnection.extIP = 0;
			}
			break;

		case 1:
			if ( msnExternalIP != NULL )
				MyConnection.extIP = inet_addr( msnExternalIP );
			else
			{
				gethostname( parBuf, sizeof( parBuf ));
				PHOSTENT myhost = gethostbyname( parBuf );
				if ( myhost != NULL )
					MyConnection.extIP = ((PIN_ADDR)myhost->h_addr)->S_un.S_addr;
			}
			MyConnection.intIP = MyConnection.extIP;
			break;

		case 2:
			MyConnection.udpConType = conUnknown;
			MyConnection.CalculateWeight();
			return;
	}

	if (getByte( "NLSpecifyOutgoingPorts", 0))
	{
		// User specified outgoing ports so the connection must be firewalled
		// do not autodetect and guess connection type from available info
		MyConnection.intIP = MyConnection.extIP;
		MyConnection.udpConType = (ConEnum)portsMapped;
		MyConnection.upnpNAT = false;
		MyConnection.CalculateWeight();
		return;
	}

	MSNatDetect();

	// If user mapped incoming ports consider direct connection
	if (portsMapped)
	{
		MSN_DebugLog("P2PNAT User manually mapped ports for incoming connection");
		switch(MyConnection.udpConType)
		{
		case conUnknown:
		case conFirewall:
		case conISALike:
			MyConnection.udpConType = conDirect;
			break;

		case conUnknownNAT:
		case conPortRestrictNAT:
		case conIPRestrictNAT:
		case conSymmetricNAT:
			MyConnection.upnpNAT = true;
			break;
		}
	}

	MSN_DebugLog("P2PNAT Connection %s found UPnP: %d ICF: %d", conStr[MyConnection.udpConType],
		MyConnection.upnpNAT, MyConnection.icf);

	MyConnection.CalculateWeight();
}


void MyConnectionType::SetUdpCon(const char* str)
{
	for (unsigned i=0; i<sizeof(conStr)/sizeof(char*); ++i)
	{
		if (strcmp(conStr[i], str) == 0)
		{
			udpConType = (ConEnum)i;
			break;
		}
	}
}


void MyConnectionType::CalculateWeight(void)
{
	if (icf) weight = 0;
	else if (udpConType == conDirect) weight = 6;
	else if (udpConType >= conIPRestrictNAT && udpConType <= conSymmetricNAT)
		weight = upnpNAT ? 5 : 2;
	else if (udpConType == conUnknownNAT)
		weight = upnpNAT ? 4 : 1;
	else if (udpConType == conUnknown) weight = 1;
	else if (udpConType == conFirewall) weight = 2;
	else if (udpConType == conISALike) weight = 3;
}
