// ---------------------------------------------------------------------------80
//                ICQ plugin for Miranda Instant Messenger
//                ________________________________________
//
// Copyright � 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede
// Copyright � 2001-2002 Jon Keating, Richard Hughes
// Copyright � 2002-2004 Martin �berg, Sam Kothari, Robert Rainwater
// Copyright � 2004-2010 Joe Kucera
// Copyright � 2012-2017 Miranda NG Team
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
// -----------------------------------------------------------------------------
//  DESCRIPTION:
//
//  OSCAR File-Transfers implementation
// -----------------------------------------------------------------------------

#include "stdafx.h"

struct oscarthreadstartinfo 
{
	int type;
	int incoming;
	MCONTACT hContact;
	HNETLIBCONN hConnection;
	DWORD dwRemoteIP;
	oscar_filetransfer *ft;
	oscar_listener *listener;
};

// small utility function
extern void NormalizeBackslash(char* path);

//
// Common functions
/////////////////////////////

char* FindFilePathContainer(const char **files, int iFile, char *szContainer)
{
	const char *szThisFile = files[iFile];
	char *szFileName = (char*)ExtractFileName(szThisFile);

	szContainer[0] = '\0';

	if (szThisFile != szFileName) { // find an earlier subdirectory to be used as a container
		for (int i = iFile - 1; i >= 0; i--) {
			size_t len = mir_strlen(files[i]);
			if (!_strnicmp(files[i], szThisFile, len) && (szThisFile[len] == '\\' || szThisFile[len] == '/')) {
				const char *pszLastBackslash;

				if (((pszLastBackslash = strrchr(files[i], '\\')) == NULL) &&
					 ((pszLastBackslash = strrchr(files[i], '/')) == NULL)) {
					mir_strcpy(szContainer, files[i]);
				}
				else {
					len = pszLastBackslash - files[i] + 1;
					null_strcpy(szContainer, szThisFile + len, szFileName - szThisFile - len);
				}
			}
		}
	}
	return szFileName;
}

//
// Utility functions
/////////////////////////////

oscar_filetransfer* CIcqProto::CreateOscarTransfer()
{
	oscar_filetransfer *ft = (oscar_filetransfer*)SAFE_MALLOC(sizeof(oscar_filetransfer));
	ft->ft_magic = FT_MAGIC_OSCAR; // Setup signature
	// Init members
	ft->fileId = -1;
	{
		mir_cslock l(oftMutex);
		m_arFileTransfers.insert(ft);
	}

	NetLog_Direct("OFT: FT struct %p created", ft);
	return ft;
}

filetransfer* CIcqProto::CreateIcqFileTransfer()
{
	filetransfer *ft = (filetransfer*)SAFE_MALLOC(sizeof(filetransfer));
	ft->ft_magic = FT_MAGIC_ICQ;
	{
		mir_cslock l(oftMutex);
		m_arFileTransfers.insert(ft);
	}

	NetLog_Direct("FT struct %p created", ft);
	return ft;
}

int CIcqProto::IsValidFileTransfer(basic_filetransfer *ft)
{
	mir_cslock l(oftMutex);
	return m_arFileTransfers.indexOf(ft) != -1;
}

int CIcqProto::IsValidOscarTransfer(basic_filetransfer *ft)
{
	mir_cslock l(oftMutex);
	return m_arFileTransfers.indexOf(ft) != -1 && ft->ft_magic == FT_MAGIC_OSCAR;
}

oscar_filetransfer* CIcqProto::FindOscarTransfer(MCONTACT hContact, DWORD dwID1, DWORD dwID2)
{
	mir_cslock l(oftMutex);

	for (int i = 0; i < m_arFileTransfers.getCount(); i++) {
		if (m_arFileTransfers[i]->ft_magic == FT_MAGIC_OSCAR) {
			oscar_filetransfer *oft = (oscar_filetransfer*)m_arFileTransfers[i];
			if (oft->hContact == hContact && oft->pMessage.dwMsgID1 == dwID1 && oft->pMessage.dwMsgID2 == dwID2)
				return oft;
		}
	}

	return NULL;
}

// Release file transfer structure
void CIcqProto::SafeReleaseFileTransfer(basic_filetransfer **bft)
{
	mir_cslock l(oftMutex);

	// Check for filetransfer validity
	int idx = m_arFileTransfers.indexOf(*bft);
	if (idx == -1)
		return;

	if (*bft) {
		if ((*bft)->ft_magic == FT_MAGIC_ICQ) { // release ICQ filetransfer structure and its contents
			filetransfer *ift = (filetransfer*)(*bft);

			SAFE_FREE(&ift->szFilename);
			SAFE_FREE(&ift->szDescription);
			SAFE_FREE(&ift->szSavePath);
			SAFE_FREE(&ift->szThisSubdir);
			if (ift->pszFiles) {
				for (int i = 0; i < (int)ift->dwFileCount; i++) {
					// szThisFile can be a duplicate of pszFiles[i]
					if (ift->szThisFile == ift->pszFiles[i])
						ift->szThisFile = NULL;
					SAFE_FREE(&ift->pszFiles[i]);
				}
				SAFE_FREE((void**)&ift->pszFiles);
			}
			SAFE_FREE(&ift->szThisFile);
			// Invalidate transfer
			m_arFileTransfers.remove(idx);

			NetLog_Direct("FT struct %p released", bft);

			// Release memory
			SAFE_FREE((void**)bft);
		}
		else if ((*bft)->ft_magic == FT_MAGIC_OSCAR) { // release oscar filetransfer structure and its contents
			oscar_filetransfer *oft = (oscar_filetransfer*)(*bft);
			// If connected, close connection
			if (oft->connection)
				CloseOscarConnection(oft->connection);
			// Release oscar listener
			if (oft->listener)
				ReleaseOscarListener((oscar_listener**)&oft->listener);
			// Release cookie
			if (oft->dwCookie)
				FreeCookie(oft->dwCookie);
			// Release all dynamic members
			SAFE_FREE(&oft->rawFileName);
			SAFE_FREE(&oft->szSavePath);
			SAFE_FREE(&oft->szThisFile);
			SAFE_FREE(&oft->szThisPath);
			SAFE_FREE(&oft->szDescription);

			if (oft->files) {
				for (int i = 0; i < oft->wFilesCount; i++)
					SAFE_FREE(&oft->files[i].szFile);
				SAFE_FREE((void**)&oft->files);
			}

			if (oft->files_list)
				SAFE_FREE((void**)&oft->files_list);

			if (oft->file_containers) {
				for (int i = 0; i < oft->containerCount; i++)
					SAFE_FREE(&oft->file_containers[i]);
				SAFE_FREE((void**)&oft->file_containers);
			}

			if (oft->fileId != -1)
				_close(oft->fileId);

			// Invalidate transfer
			m_arFileTransfers.remove(idx);

			NetLog_Direct("OFT: FT struct %p released", bft);

			// Release memory
			SAFE_FREE((void**)bft);
		}
	}
}

// Calculate oft checksum of buffer
// --------------------------------
// Information was gathered from Gaim's sources, thanks
//
DWORD oft_calc_checksum(int offset, const BYTE *buffer, int len, DWORD dwChecksum)
{
	DWORD checksum = (dwChecksum >> 16) & 0xffff;

	for (int i = 0; i < len; i++) {
		WORD val = buffer[i];

		if (((i + offset) & 1) == 0)
			val = val << 8;

		if (checksum < val)
			checksum -= val + 1;
		else // simulate carry
			checksum -= val;
	}
	checksum = ((checksum & 0x0000ffff) + (checksum >> 16));
	checksum = ((checksum & 0x0000ffff) + (checksum >> 16));
	return checksum << 16;
}

DWORD oft_calc_file_checksum(int hFile, __int64 maxSize)
{
	BYTE buf[OFT_BUFFER_SIZE];
	int bytesRead;
	__int64 offset = 0;
	DWORD dwCheck = 0xFFFF0000;

	_lseek(hFile, 0, SEEK_SET);
	bytesRead = _read(hFile, buf, (maxSize < sizeof(buf)) ? maxSize : (unsigned)sizeof(buf));
	if (bytesRead == -1)
		return dwCheck;

	while (bytesRead) {
		dwCheck = oft_calc_checksum((int)offset, buf, bytesRead, dwCheck);
		offset += bytesRead;
		bytesRead = _read(hFile, buf, sizeof(buf));
		if (bytesRead + offset > maxSize) bytesRead = (int)(maxSize - offset);
	}
	_lseek(hFile, 0, SEEK_SET); // back to beginning

	return dwCheck;
}

oscar_listener* CIcqProto::CreateOscarListener(oscar_filetransfer *ft, NETLIBNEWCONNECTIONPROC_V2 handler)
{
	oscar_listener *listener = (oscar_listener*)SAFE_MALLOC(sizeof(oscar_listener));
	if (listener) {
		listener->ppro = this;
		listener->ft = ft;
		if (listener->hBoundPort = NetLib_BindPort(handler, listener, &listener->wPort, NULL))
			return listener; // Success

		SAFE_FREE((void**)&listener);
	}

	return NULL; // Failure
}


void CIcqProto::ReleaseOscarListener(oscar_listener **pListener)
{
	oscar_listener *listener = *pListener;
	if (listener) { // Close listening port
		if (listener->hBoundPort) {
			Netlib_CloseHandle(listener->hBoundPort);
			listener->hBoundPort = NULL;
		}

		NetLog_Direct("Oscar listener on port %d released.", listener->wPort);
	}
	SAFE_FREE((void**)pListener);
}


//
// Miranda FT interface handlers & services
/////////////////////////////

void CIcqProto::handleRecvServMsgOFT(BYTE *buf, size_t wLen, DWORD dwUin, char *szUID, DWORD dwID1, DWORD dwID2, WORD wCommand)
{
	MCONTACT hContact = HContactFromUID(dwUin, szUID, NULL);

	if (wCommand == 0) { // this is OFT request
		oscar_tlv_chain* chain = readIntoTLVChain(&buf, wLen, 0);

		if (chain) {
			WORD wAckType = chain->getWord(0x0A, 1);

			if (wAckType == 1) { // This is first request in this OFT
				oscar_filetransfer *ft = CreateOscarTransfer();
				char *pszFileName = NULL;
				char *pszDescription = NULL;

				debugLogA("This is a file request");

				// This TLV chain may contain the following TLVs:
				// TLV(A): Acktype 0x0001 - file request / abort request
				//                 0x0002 - file ack
				// TLV(F): Unknown
				// TLV(E): Language ?
				// TLV(2): Proxy IP
				// TLV(16): Proxy IP Check
				// TLV(3): External IP
				// TLV(4): Internal IP
				// TLV(5): Port
				// TLV(17): Port Check
				// TLV(10): Proxy Flag
				// TLV(D): Charset of User Message
				// TLV(C): User Message (ICQ_COOL_FT)
				// TLV(2711): FT info
				// TLV(2712): Charset of file name

				// init filetransfer structure
				ft->pMessage.dwMsgID1 = dwID1;
				ft->pMessage.dwMsgID2 = dwID2;
				ft->bUseProxy = chain->getTLV(0x10, 1) ? 1 : 0;
				ft->dwProxyIP = chain->getDWord(0x02, 1);
				ft->dwRemoteInternalIP = chain->getDWord(0x03, 1);
				ft->dwRemoteExternalIP = chain->getDWord(0x04, 1);
				ft->wRemotePort = chain->getWord(0x05, 1);
				ft->wReqNum = wAckType;

				{ // User Message
					oscar_tlv* tlv = chain->getTLV(0x0C, 1);

					if (tlv) { // parse User Message
						BYTE* tBuf = tlv->pData;

						pszDescription = (char*)_alloca(tlv->wLen + 2);
						unpackString(&tBuf, (char*)pszDescription, tlv->wLen);
						pszDescription[tlv->wLen] = '\0';
						pszDescription[tlv->wLen + 1] = '\0';
						{ // apply User Message encoding
							oscar_tlv *charset = chain->getTLV(0x0D, 1);
							char *str = pszDescription;
							char *bTag, *eTag;

							if (charset) { // decode charset
								char *szEnc = (char*)_alloca(charset->wLen + 1);

								null_strcpy(szEnc, (char*)charset->pData, charset->wLen);
								str = ApplyEncoding((char*)pszDescription, szEnc);
							}
							else str = null_strdup(str);
							
							// eliminate HTML tags
							pszDescription = EliminateHtml(str, mir_strlen(str));

							bTag = strstrnull(pszDescription, "<DESC>");
							if (bTag) { // take special Description - ICQJ's extension
								eTag = strstrnull(bTag, "</DESC>");
								if (eTag) {
									*eTag = '\0';
									str = null_strdup(bTag + 6);
									SAFE_FREE(&pszDescription);
									pszDescription = str;
								}
							}
							else {
								bTag = strstrnull(pszDescription, "<FS>");
								if (bTag) { // take only <FS> - Description tag if present
									eTag = strstrnull(bTag, "</FS>");
									if (eTag) {
										*eTag = '\0';
										str = null_strdup(bTag + 4);
										SAFE_FREE(&pszDescription);
										pszDescription = str;
									}
								}
							}
						}
					}
					if (!mir_strlen(pszDescription)) {
						SAFE_FREE(&pszDescription);
						pszDescription = ICQTranslateUtf(LPGEN("No description given"));
					}
				}
				{ // parse File Transfer Info block
					oscar_tlv* tlv = chain->getTLV(0x2711, 1);

					// sanity check
					if (!tlv || tlv->wLen < 8) {
						debugLogA("Error: Malformed file request");
						// release structures
						SafeReleaseFileTransfer((basic_filetransfer**)&ft);
						SAFE_FREE(&pszDescription);
						return;
					}

					BYTE  *tBuf = tlv->pData;
					size_t tLen = tlv->wLen;
					WORD   wFlag;

					unpackWord(&tBuf, &wFlag); // FT flag
					unpackWord(&tBuf, &ft->wFilesCount);
					unpackDWord(&tBuf, (DWORD*)&ft->qwTotalSize);
					tLen -= 8;
					// Filename / Directory Name
					if (tLen) { // some filename specified, unpack
						size_t wFilenameLength = tLen - 1;
						pszFileName = (char*)_alloca(tLen);
						unpackString(&tBuf, (char*)pszFileName, wFilenameLength);
						pszFileName[wFilenameLength] = '\0';
					}
					else if (ft->wFilesCount == 1) // give some generic file name
						pszFileName = "unnamed_file";
					else // or empty directory name
						pszFileName = "";

					// apply Filename / Directory Name encoding
					oscar_tlv* charset = chain->getTLV(0x2712, 1);
					if (charset) {
						char* szEnc = (char*)_alloca(charset->wLen + 1);
						null_strcpy(szEnc, (char*)charset->pData, charset->wLen);
						pszFileName = ApplyEncoding(pszFileName, szEnc);
					}
					else pszFileName = ansi_to_utf8(pszFileName);

					if (ft->wFilesCount == 1) {  // Filename - use for DB event
						char *szFileName = (char*)_alloca(mir_strlen(pszFileName) + 1);

						mir_strcpy(szFileName, pszFileName);
						SAFE_FREE(&pszFileName);
						pszFileName = szFileName;
					}
					else {  // Save Directory name for future use
						ft->szThisPath = pszFileName;
						// for multi-file transfer we do not display "folder" name, but create only a simple notice
						pszFileName = (char*)_alloca(64);

						char tmp[64];
						mir_snprintf(pszFileName, 64, ICQTranslateUtfStatic(LPGEN("%d Files"), tmp, _countof(tmp)), ft->wFilesCount);
					}
				}
				// Total Size TLV (ICQ 6 and AIM 6)
				{
					oscar_tlv *tlv = chain->getTLV(0x2713, 1);

					if (tlv && tlv->wLen >= 8) {
						BYTE *tBuf = tlv->pData;

						unpackQWord(&tBuf, &ft->qwTotalSize);
					}
				}
				int bAdded;
				ft->hContact = HContactFromUID(dwUin, szUID, &bAdded);
				ft->fileId = -1;

				// Send chain event
				char *szBlob = (char*)_alloca(sizeof(DWORD) + mir_strlen(pszFileName) + mir_strlen(pszDescription) + 2);
				*(PDWORD)szBlob = 0;
				mir_strcpy(szBlob + sizeof(DWORD), pszFileName);
				mir_strcpy(szBlob + sizeof(DWORD) + mir_strlen(pszFileName) + 1, pszDescription);

				wchar_t* ptszFileName = mir_utf8decodeW(pszFileName);

				PROTORECVFILET pre = { 0 };
				pre.dwFlags = PRFF_UNICODE;
				pre.fileCount = 1;
				pre.timestamp = time(NULL);
				pre.descr.w = mir_utf8decodeW(pszDescription);
				pre.files.w = &ptszFileName;
				pre.lParam = (LPARAM)ft;
				ProtoChainRecvFile(ft->hContact, &pre);

				mir_free(pre.descr.w);
				mir_free(ptszFileName);
			}
			else if (wAckType == 2) { // First attempt failed, reverse requested
				oscar_filetransfer *ft = FindOscarTransfer(hContact, dwID1, dwID2);
				if (ft) {
					NetLog_Direct("OFT: Redirect received (%d)", wAckType);

					ft->wReqNum = wAckType;

					if (ft->flags & OFTF_SENDING) {
						ReleaseOscarListener((oscar_listener**)&ft->listener);

						ft->bUseProxy = chain->getTLV(0x10, 1) ? 1 : 0;
						ft->dwProxyIP = chain->getDWord(0x02, 1);
						ft->dwRemoteInternalIP = chain->getDWord(0x03, 1);
						ft->dwRemoteExternalIP = chain->getDWord(0x04, 1);
						ft->wRemotePort = chain->getWord(0x05, 1);

						OpenOscarConnection(hContact, ft, ft->bUseProxy ? OCT_PROXY_RECV : OCT_REVERSE);
					}
					else { // Just sanity
						ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)ft, 0);
						// Release transfer
						SafeReleaseFileTransfer((basic_filetransfer**)&ft);
					}
				}
				else debugLogA("Error: Invalid request, no such transfer");
			}
			else if (wAckType == 3) { // Transfering thru proxy, join tunnel
				oscar_filetransfer *ft = FindOscarTransfer(hContact, dwID1, dwID2);
				if (ft) { // release possible previous listener
					NetLog_Direct("OFT: Redirect received (%d)", wAckType);

					ft->wReqNum = wAckType;

					ReleaseOscarListener((oscar_listener**)&ft->listener);

					ft->bUseProxy = chain->getTLV(0x10, 1) ? 1 : 0;
					ft->dwProxyIP = chain->getDWord(0x02, 1);
					ft->wRemotePort = chain->getWord(0x05, 1);

					if (ft->bUseProxy && ft->dwProxyIP) // Init proxy connection
						OpenOscarConnection(hContact, ft, OCT_PROXY_RECV);
					else // try Stage 4
						OpenOscarConnection(hContact, ft, OCT_PROXY);
				}
				else debugLogA("Error: Invalid request, no such transfer");
			}
			else if (wAckType == 4) {
				oscar_filetransfer *ft = FindOscarTransfer(hContact, dwID1, dwID2);
				if (ft) {
					NetLog_Direct("OFT: Redirect received (%d)", wAckType);

					ft->wReqNum = wAckType;
					ft->bUseProxy = chain->getTLV(0x10, 1) ? 1 : 0;
					ft->dwProxyIP = chain->getDWord(0x02, 1);
					ft->wRemotePort = chain->getWord(0x05, 1);

					if (ft->bUseProxy && ft->dwProxyIP) // Init proxy connection
						OpenOscarConnection(hContact, ft, OCT_PROXY_RECV);
					else
						debugLogA("Error: Invalid request, IP missing.");
				}
				else debugLogA("Error: Invalid request, no such transfer");
			}
			else debugLogA("Error: Uknown Stage %d request", wAckType);

			disposeChain(&chain);
		}
		else debugLogA("Error: Missing TLV chain in OFT request");
	}
	else if (wCommand == 1) { // transfer cancelled/aborted
		oscar_filetransfer *ft = FindOscarTransfer(hContact, dwID1, dwID2);
		if (ft) {
			debugLogA("OFT: File transfer cancelled by %s", strUID(dwUin, szUID));

			ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)ft, 0);
			// Notify user, that the FT was cancelled // TODO: new ACKRESULT_?
			icq_LogMessage(LOG_ERROR, LPGEN("The file transfer was aborted by the other user."));
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&ft);
		}
		else debugLogA("Error: Invalid request, no such transfer");
	}
	else if (wCommand == 2) { // transfer accepted - connection established
		oscar_filetransfer *ft = FindOscarTransfer(hContact, dwID1, dwID2);

		if (ft) {
			NetLog_Direct("OFT: Session established.");
			// Init connection
			if (ft->flags & OFTF_SENDING) {
				if (ft->connection && ft->connection->status == OCS_CONNECTED) {
					if (!(ft->flags & OFTF_FILE_REQUEST_SENT)) {
						ft->flags |= OFTF_FILE_REQUEST_SENT;
						// proceed with first file
						oft_sendPeerInit(ft->connection);
					}
				}
				ft->flags |= OFTF_INITIALIZED; // accept was received
			}
			else debugLogA("Warning: Received invalid rendezvous accept");
		}
		else debugLogA("Error: Invalid request, no such transfer");
	}
	else debugLogA("Error: Unknown wCommand=0x%x in OFT request", wCommand);
}

void CIcqProto::handleRecvServResponseOFT(BYTE *buf, size_t wLen, DWORD dwUin, char *szUID, void* ft)
{
	if (wLen < 2)
		return;

	size_t wDataLen;
	unpackWord(&buf, &wDataLen);

	if (wDataLen == 2) {
		oscar_filetransfer *oft = (oscar_filetransfer*)ft;

		WORD wStatus;
		unpackWord(&buf, &wStatus);

		switch (wStatus) {
		case 1:
			// FT denied (icq5)
			debugLogA("OFT: File transfer denied by %s", strUID(dwUin, szUID));

			ProtoBroadcastAck(oft->hContact, ACKTYPE_FILE, ACKRESULT_DENIED, (HANDLE)oft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&oft);
			break;

		case 4: // Proxy error
			icq_LogMessage(LOG_ERROR, LPGEN("The file transfer failed: Proxy error"));

			ProtoBroadcastAck(oft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)oft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&oft);
			break;

		case 5: // Invalid request
			icq_LogMessage(LOG_ERROR, LPGEN("The file transfer failed: Invalid request"));

			ProtoBroadcastAck(oft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)oft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&oft);
			break;

		case 6: // Proxy Failed (IP = 0)
			icq_LogMessage(LOG_ERROR, LPGEN("The file transfer failed: Proxy unavailable"));

			ProtoBroadcastAck(oft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)oft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&oft);
			break;

		default:
			debugLogA("OFT: Uknown request response code 0x%x", wStatus);

			ProtoBroadcastAck(oft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)oft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&oft);
		}
	}
}

// This function is called from the Netlib when someone is connecting to our oscar_listener
static void oft_newConnectionReceived(HNETLIBCONN hNewConnection, DWORD dwRemoteIP, void *pExtra)
{
	oscarthreadstartinfo *otsi = (oscarthreadstartinfo*)SAFE_MALLOC(sizeof(oscarthreadstartinfo));
	oscar_listener *listener = (oscar_listener*)pExtra;

	otsi->type = listener->ft->flags & OFTF_SENDING ? OCT_NORMAL : OCT_REVERSE;
	otsi->incoming = 1;
	otsi->hConnection = hNewConnection;
	otsi->dwRemoteIP = dwRemoteIP;
	otsi->listener = listener;

	// Start a new thread for the incomming connection
	listener->ppro->ForkThread((CIcqProto::MyThreadFunc)&CIcqProto::oft_connectionThread, otsi);
}

static char* oftGetFileContainer(oscar_filetransfer* oft, const char** files, int iFile)
{
	char szPath[MAX_PATH];
	FindFilePathContainer(files, iFile, szPath);
	char *szPathUtf = ansi_to_utf8(szPath);
	int i;

	// try to find existing container
	for (i = 0; i < oft->containerCount; i++)
		if (!mir_strcmp(szPathUtf, oft->file_containers[i])) {
			SAFE_FREE((void**)&szPathUtf);
			return oft->file_containers[i];
		}

	// create new container
	i = oft->containerCount++;
	oft->file_containers = (char**)SAFE_REALLOC(oft->file_containers, (sizeof(char*) * oft->containerCount));
	oft->file_containers[i] = szPathUtf;

	return oft->file_containers[i];
}


HANDLE CIcqProto::oftInitTransfer(MCONTACT hContact, DWORD dwUin, char* szUid, const wchar_t** files, const wchar_t* pszDesc)
{
	int i, filesCount;
	struct _stati64 statbuf;

	// Initialize filetransfer struct
	debugLogA("Init file send");

	oscar_filetransfer *ft = CreateOscarTransfer();
	ft->hContact = hContact;
	ft->pMessage.bMessageType = MTYPE_FILEREQ;
	InitMessageCookie(&ft->pMessage);

	for (filesCount = 0; files[filesCount]; filesCount++);
	ft->files = (oft_file_record *)SAFE_MALLOC(sizeof(oft_file_record) * filesCount);
	ft->files_list = (char**)SAFE_MALLOC(sizeof(wchar_t *) * filesCount);
	ft->qwTotalSize = 0;

	char **filesUtf = (char**)SAFE_MALLOC(sizeof(char *) * filesCount);
	for (i = 0; i < filesCount; i++) filesUtf[i] = FileNameToUtf(files[i]);

	// Prepare files arrays
	for (i = 0; i < filesCount; i++) {
		if (_wstat64(files[i], &statbuf))
			debugLogA("IcqSendFile() was passed invalid filename \"%s\"", files[i]);
		else {
			if (!(statbuf.st_mode & _S_IFDIR)) { // take only files
				ft->files[ft->wFilesCount].szFile = ft->files_list[ft->wFilesCount] = null_strdup(filesUtf[i]);
				ft->files[ft->wFilesCount].szContainer = oftGetFileContainer(ft, (LPCSTR*)filesUtf, i);

				ft->wFilesCount++;
				ft->qwTotalSize += statbuf.st_size;
			}
		}
	}

	for (i = 0; i < filesCount; i++)
		SAFE_FREE(&filesUtf[i]);
	SAFE_FREE((void**)&filesUtf);

	if (!ft->wFilesCount) { // found no valid files to send
		icq_LogMessage(LOG_ERROR, LPGEN("Failed to Initialize File Transfer. No valid files were specified."));
		// Notify UI
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)ft, 0);

		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&ft);
		return 0; // Failure
	}
#ifdef __GNUC__
#define OSCAR_MAX_SIZE 0x100000000ULL
#else
#define OSCAR_MAX_SIZE 0x100000000
#endif
	if (ft->qwTotalSize >= OSCAR_MAX_SIZE && ft->wFilesCount > 1) { // file larger than 4GB can be send only as single
		icq_LogMessage(LOG_ERROR, LPGEN("The files are too big to be sent at once. Files bigger than 4 GB can be sent only separately."));
		// Notify UI
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)ft, 0);

		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&ft);
		return 0; // Failure
	}

	debugLogA("OFT: Found %d files.", ft->wFilesCount);

	ft->szDescription = make_utf8_string(pszDesc);
	ft->flags = OFTF_SENDING;
	ft->fileId = -1;
	ft->iCurrentFile = 0;
	ft->dwCookie = AllocateCookie(CKT_FILE, ICQ_MSG_SRV_SEND, hContact, ft);

	// Init oscar fields
	{
		ft->wEncrypt = 0;
		ft->wCompress = 0;
		ft->wPartsCount = 1;
		ft->wPartsLeft = 1;
		mir_strcpy(ft->rawIDString, "Cool FileXfer");
		ft->bHeaderFlags = 0x20;
		ft->bNameOff = 0x1C;
		ft->bSizeOff = 0x11;
		ft->dwRecvForkCheck = 0xFFFF0000;
		ft->dwThisForkCheck = 0xFFFF0000;
		ft->dwRecvFileCheck = 0xFFFF0000;
	}

	// Send file transfer request
	{
		char *pszFiles;

		if (ft->wFilesCount == 1) { // transfering single file, give filename
			pszFiles = (char*)ExtractFileName(ft->files[0].szFile);
		}
		else { // check if transfering one directory
			char *szFirstDiv, *szFirstDir = ft->file_containers[0];
			size_t nFirstDirLen;

			// default is no root dir
			pszFiles = "";

			if ((szFirstDiv = strstrnull(szFirstDir, "\\")) || (szFirstDiv = strstrnull(szFirstDir, "/")))
				nFirstDirLen = szFirstDiv - szFirstDir;
			else
				nFirstDirLen = mir_strlen(szFirstDir);

			if (nFirstDirLen) { // got root dir from first container, check if others are only sub-dirs
				for (i = 0; i < ft->containerCount; i++) {
					if (_strnicmp((char*)ft->file_containers[i], (char*)szFirstDir, nFirstDirLen)) {
						szFirstDir = NULL;
						break;
					}
				}
				if (szFirstDir) { // fine, we are sending only one directory
					pszFiles = szFirstDir;
					if (szFirstDiv) szFirstDiv[0] = '\0';
					nFirstDirLen++; // include backslash
					// cut all files container by root dir - it is transferred as root separately
					for (i = 0; i < ft->wFilesCount; i++)
						ft->files[i].szContainer += nFirstDirLen;
				}
			}
		}

		// Create listener
		ft->listener = CreateOscarListener(ft, oft_newConnectionReceived);

		// Send packet
		if (ft->listener) {
			oft_sendFileRequest(dwUin, szUid, ft, pszFiles, getDword("RealIP", 0));
		}
		else { // try stage 1 proxy
			ft->szThisFile = null_strdup(pszFiles);
			OpenOscarConnection(hContact, ft, OCT_PROXY_INIT);
		}
	}

	return ft; // Success
}


HANDLE CIcqProto::oftFileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath)
{
	oscar_filetransfer *ft = (oscar_filetransfer*)hTransfer;

	DWORD dwUin;
	uid_str szUid;
	if (getContactUid(hContact, &dwUin, &szUid))
		return 0; // Invalid contact

	if (!IsValidOscarTransfer(ft))
		return 0; // Invalid transfer

	ft->szSavePath = make_utf8_string(szPath);

	if (ft->szThisPath) { // Append Directory name to the save path, when transfering a directory
		ft->szSavePath = (char*)SAFE_REALLOC(ft->szSavePath, mir_strlen(ft->szSavePath) + mir_strlen(ft->szThisPath) + 4);
		NormalizeBackslash(ft->szSavePath);
		mir_strcat(ft->szSavePath, ft->szThisPath);
		NormalizeBackslash(ft->szSavePath);
	}

	NetLog_Direct("OFT: Request accepted, saving to '%s'.", ft->szSavePath);

	// Create cookie
	ft->dwCookie = AllocateCookie(CKT_FILE, ICQ_MSG_SRV_SEND, hContact, ft);

	OpenOscarConnection(hContact, ft, ft->bUseProxy ? OCT_PROXY_RECV : OCT_NORMAL);
	return hTransfer; // Success
}

DWORD CIcqProto::oftFileDeny(MCONTACT hContact, HANDLE hTransfer, const wchar_t*)
{
	oscar_filetransfer *ft = (oscar_filetransfer*)hTransfer;

	DWORD dwUin;
	uid_str szUid;
	if (getContactUid(hContact, &dwUin, &szUid))
		return 1; // Invalid contact

	if (!IsValidOscarTransfer(ft))
		return 1; // Invalid transfer
		
	if (ft->hContact != hContact)
		return 1; // Bad contact or hTransfer

	NetLog_Direct("OFT: Request denied.");

	oft_sendFileDeny(dwUin, szUid, ft);

	// Release structure
	SafeReleaseFileTransfer((basic_filetransfer**)&ft);
	return 0; // Success
}

DWORD CIcqProto::oftFileCancel(MCONTACT hContact, HANDLE hTransfer)
{
	oscar_filetransfer* ft = (oscar_filetransfer*)hTransfer;

	DWORD dwUin;
	uid_str szUid;
	if (getContactUid(hContact, &dwUin, &szUid))
		return 1; // Invalid contact

	if (!IsValidOscarTransfer(ft))
		return 1; // Invalid transfer
	
	if (ft->hContact != hContact)
		return 1; // Bad contact or hTransfer

	NetLog_Direct("OFT: Transfer cancelled.");

	oft_sendFileCancel(dwUin, szUid, ft);

	ProtoBroadcastAck(hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);

	// Release structure
	SafeReleaseFileTransfer((basic_filetransfer**)&ft);

	return 0; // Success
}

void CIcqProto::oftFileResume(oscar_filetransfer *ft, int action, const wchar_t *szFilename)
{
	int openFlags;

	if (ft->connection == NULL)
		return;

	oscar_connection *oc = ft->connection;

	NetLog_Direct("OFT: Resume Transfer, Action: %d, FileName: '%s'", action, szFilename);

	switch (action) {
	case FILERESUME_RESUME:
		openFlags = _O_BINARY | _O_RDWR;
		break;

	case FILERESUME_OVERWRITE:
		openFlags = _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY;
		ft->qwFileBytesDone = 0;
		break;

	case FILERESUME_SKIP:
		openFlags = _O_BINARY | _O_WRONLY;
		ft->qwFileBytesDone = ft->qwThisFileSize;
		break;

	case FILERESUME_RENAME:
		openFlags = _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY;
		SAFE_FREE(&ft->szThisFile);
		ft->szThisFile = make_utf8_string(szFilename);
		ft->qwFileBytesDone = 0;
		break;

	default: // workaround for bug in Miranda Core
		if (ft->resumeAction == FILERESUME_RESUME)
			openFlags = _O_BINARY | _O_RDWR;
		else { // default to overwrite
			openFlags = _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY;
			ft->qwFileBytesDone = 0;
		}
	}
	ft->resumeAction = action;

	ft->fileId = OpenFileUtf(ft->szThisFile, openFlags, _S_IREAD | _S_IWRITE);
	if (ft->fileId == -1) {
		NetLog_Direct("OFT: errno=%d", errno);

		icq_LogMessage(LOG_ERROR, LPGEN("Your file receive has been aborted because Miranda could not open the destination file in order to write to it. You may be trying to save to a read-only folder."));

		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
		return;
	}

	if (action == FILERESUME_RESUME)
		ft->qwFileBytesDone = _lseeki64(ft->fileId, 0, SEEK_END);
	else
		_lseeki64(ft->fileId, ft->qwFileBytesDone, SEEK_SET);

	ft->qwBytesDone += ft->qwFileBytesDone;

	if (action == FILERESUME_RESUME) { // use smart-resume
		oc->status = OCS_RESUME;
		ft->dwRecvFileCheck = oft_calc_file_checksum(ft->fileId, ft->qwFileBytesDone);
		_lseek(ft->fileId, 0, SEEK_END);

		NetLog_Direct("OFT: Starting Smart-Resume");

		sendOFT2FramePacket(oc, OFT_TYPE_RESUMEREQUEST);
		return;
	}
	
	if (action == FILERESUME_SKIP) // we are skipping the file, send "we are done"
		oc->status = OCS_NEGOTIATION;

	else { // Send "we are ready"
		oc->status = OCS_DATA;
		ft->flags |= OFTF_FILE_RECEIVING;

		sendOFT2FramePacket(oc, OFT_TYPE_READY);
	}
	ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);

	if (!ft->qwThisFileSize || action == FILERESUME_SKIP) { // if the file is empty we will not receive any data
		BYTE buf;
		oft_handleFileData(oc, &buf, 0);
	}
}


static void oft_buildProtoFileTransferStatus(oscar_filetransfer* ft, PROTOFILETRANSFERSTATUS* pfts)
{
	memset(pfts, 0, sizeof(PROTOFILETRANSFERSTATUS));
	pfts->cbSize = sizeof(PROTOFILETRANSFERSTATUS);
	pfts->hContact = ft->hContact;
	pfts->flags = PFTS_UTF + ((ft->flags & OFTF_SENDING) ? PFTS_SENDING : PFTS_RECEIVING);
	if (ft->flags & OFTF_SENDING)
		pfts->pszFiles = ft->files_list;
	else
		pfts->pszFiles = NULL;  /* FIXME */
	pfts->totalFiles = ft->wFilesCount;
	pfts->currentFileNumber = ft->iCurrentFile;
	pfts->totalBytes = ft->qwTotalSize;
	pfts->totalProgress = ft->qwBytesDone;
	pfts->szWorkingDir = ft->szThisPath;
	pfts->szCurrentFile = ft->szThisFile;
	pfts->currentFileSize = ft->qwThisFileSize;
	pfts->currentFileTime = ft->dwThisFileDate;
	pfts->currentFileProgress = ft->qwFileBytesDone;
}

void CIcqProto::CloseOscarConnection(oscar_connection *oc)
{
	mir_cslock l(oftMutex);

	if (oc) {
		oc->type = OCT_CLOSING;
		if (oc->hConnection)  // we need this for Netlib handle consistency
			NetLib_CloseConnection(&oc->hConnection, FALSE);
	}
}

/////////////////////////////////////////////////////////////////////////////////////////

void CIcqProto::OpenOscarConnection(MCONTACT hContact, oscar_filetransfer *ft, int type)
{
	oscarthreadstartinfo *otsi = (oscarthreadstartinfo*)SAFE_MALLOC(sizeof(oscarthreadstartinfo));

	otsi->hContact = hContact;
	otsi->type = type;
	otsi->ft = ft;

	ForkThread((MyThreadFunc)&CIcqProto::oft_connectionThread, otsi);
}

int CIcqProto::CreateOscarProxyConnection(oscar_connection *oc)
{
	// inform UI
	ProtoBroadcastAck(oc->ft->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTPROXY, oc->ft, 0);

	NETLIBOPENCONNECTION nloc = { 0 };
	nloc.szHost = OSCAR_PROXY_HOST;
	nloc.wPort = getWord("OscarPort", m_bSecureConnection ? DEFAULT_SERVER_PORT_SSL : DEFAULT_SERVER_PORT);
	if (nloc.wPort == 0)
		nloc.wPort = RandRange(1024, 65535);
	if (m_bGatewayMode)
		nloc.flags |= NLOCF_HTTPGATEWAY;

	oc->hConnection = NetLib_OpenConnection(m_hNetlibUser, "Proxy ", &nloc);
	if (!oc->hConnection) // proxy connection failed
		return 0;

	oc->type = OCT_PROXY;
	oc->status = OCS_PROXY;
	oc->ft->connection = oc;

	// init proxy
	proxy_sendInitTunnel(oc);
	return 1; // Success
}

void __cdecl CIcqProto::oft_connectionThread(oscarthreadstartinfo *otsi)
{
	oscar_connection oc = { 0 };
	oscar_listener *source;

	Thread_SetName("ICQ: oft_connectionThread");

	oc.hContact = otsi->hContact;
	oc.hConnection = otsi->hConnection;
	oc.type = otsi->type;
	oc.incoming = otsi->incoming;
	oc.ft = otsi->ft;
	source = otsi->listener;
	if (oc.incoming) {
		if (IsValidOscarTransfer(source->ft)) {
			oc.ft = source->ft;
			oc.ft->dwRemoteExternalIP = otsi->dwRemoteIP;
			oc.hContact = oc.ft->hContact;
			oc.ft->connection = &oc;
			oc.status = OCS_CONNECTED;
		}
		else { // FT is already over, kill listener
			NetLog_Direct("Received unexpected connection, closing.");

			CloseOscarConnection(&oc);
			ReleaseOscarListener(&source);

			SAFE_FREE((void**)&otsi);
			return;
		}
	}
	SAFE_FREE((void**)&otsi);

	if (oc.hContact)  // Load contact information
		getContactUid(oc.hContact, &oc.dwUin, &oc.szUid);

	// Load local IP information
	oc.dwLocalExternalIP = getDword("IP", 0);
	oc.dwLocalInternalIP = getDword("RealIP", 0);

	if (!oc.incoming) { // create outgoing connection
		if (oc.type == OCT_NORMAL || oc.type == OCT_REVERSE) { // create outgoing connection to peer
			NETLIBOPENCONNECTION nloc = { 0 };
			IN_ADDR addr = { 0 }, addr2 = { 0 };

			if (oc.ft->dwRemoteExternalIP == oc.dwLocalExternalIP && oc.ft->dwRemoteInternalIP)
				addr.S_un.S_addr = htonl(oc.ft->dwRemoteInternalIP);
			else if (oc.ft->dwRemoteExternalIP) {
				addr.S_un.S_addr = htonl(oc.ft->dwRemoteExternalIP);
				// for different internal, try it also (for LANs with multiple external IP, VPNs, etc.)
				if (oc.ft->dwRemoteInternalIP != oc.ft->dwRemoteExternalIP)
					addr2.S_un.S_addr = htonl(oc.ft->dwRemoteInternalIP);
			}
			else // try LAN
				addr.S_un.S_addr = htonl(oc.ft->dwRemoteInternalIP);

			// Inform UI that we will attempt to connect
			ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTING, oc.ft, 0);

			if (!addr.S_un.S_addr && oc.type == OCT_NORMAL) { // IP to connect to is empty, request reverse
				oscar_listener* listener = CreateOscarListener(oc.ft, oft_newConnectionReceived);

				if (listener) { // we got listening port, fine send request
					oc.ft->listener = listener;
					// notify UI
					ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_LISTENING, oc.ft, 0);

					oft_sendFileRedirect(oc.dwUin, oc.szUid, oc.ft, oc.dwLocalInternalIP, listener->wPort, FALSE);
					return;
				}
				if (!CreateOscarProxyConnection(&oc)) { // normal connection failed, notify peer, wait for error or stage 3 proxy
					oft_sendFileRedirect(oc.dwUin, oc.szUid, oc.ft, 0, 0, FALSE);
					// stage 3 can follow
					return;
				}
			}
			else if (addr.S_un.S_addr && oc.ft->wRemotePort) {
				nloc.szHost = inet_ntoa(addr);
				nloc.wPort = oc.ft->wRemotePort;
				nloc.timeout = 8; // 8 secs to connect
				oc.hConnection = NetLib_OpenConnection(m_hDirectNetlibUser, oc.type == OCT_REVERSE ? "Reverse " : NULL, &nloc);
				if (!oc.hConnection && addr2.S_un.S_addr) { // first address failed, try second one if available
					nloc.szHost = inet_ntoa(addr2);
					oc.hConnection = NetLib_OpenConnection(m_hDirectNetlibUser, oc.type == OCT_REVERSE ? "Reverse " : NULL, &nloc);
				}
				if (!oc.hConnection) {
					if (oc.type == OCT_NORMAL) { // connection failed, try reverse
						oscar_listener* listener = CreateOscarListener(oc.ft, oft_newConnectionReceived);

						if (listener) { // we got listening port, fine send request
							oc.ft->listener = listener;
							// notify UI that we await connection
							ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_LISTENING, oc.ft, 0);

							oft_sendFileRedirect(oc.dwUin, oc.szUid, oc.ft, oc.dwLocalInternalIP, listener->wPort, FALSE);
							return;
						}
					}
					if (!CreateOscarProxyConnection(&oc)) { // proxy connection failed, notify peer, wait for error or stage 4 proxy
						oft_sendFileRedirect(oc.dwUin, oc.szUid, oc.ft, 0, 0, FALSE);
						// stage 3 or stage 4 can follow
						return;
					}
				}
				else {
					oc.status = OCS_CONNECTED;
					// ack normal connection
					oc.ft->connection = &oc;
					// acknowledge OFT - connection is ready
					oft_sendFileAccept(oc.dwUin, oc.szUid, oc.ft);
					// signal UI
					ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, oc.ft, 0);
				}
			}
			else { // try proxy, stage 3 (sending)
				if (!CreateOscarProxyConnection(&oc)) { // proxy connection failed, notify peer, wait for error or stage 4 proxy
					oft_sendFileRedirect(oc.dwUin, oc.szUid, oc.ft, 0, 0, FALSE);
					// stage 4 can follow
					return;
				}
			}
		}
		else if (oc.type == OCT_PROXY_RECV) { // stage 2 & stage 4
			if (oc.ft->dwProxyIP && oc.ft->wRemotePort) { // create proxy connection, join tunnel
				NETLIBOPENCONNECTION nloc = { 0 };
				IN_ADDR addr = { 0 };

				// inform UI that we will connect to file proxy
				ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTPROXY, oc.ft, 0);

				addr.S_un.S_addr = htonl(oc.ft->dwProxyIP);
				nloc.szHost = inet_ntoa(addr);
				nloc.wPort = getWord("OscarPort", m_bSecureConnection ? DEFAULT_SERVER_PORT_SSL : DEFAULT_SERVER_PORT);
				if (nloc.wPort == 0)
					nloc.wPort = RandRange(1024, 65535);
				if (m_bGatewayMode)
					nloc.flags |= NLOCF_HTTPGATEWAY;
				oc.hConnection = NetLib_OpenConnection(m_hNetlibUser, "Proxy ", &nloc);
				if (!oc.hConnection) { // proxy connection failed, we are out of possibilities
					ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc.ft, 0);
					// notify the other side, that we failed
					oft_sendFileResponse(oc.dwUin, oc.szUid, oc.ft, 0x04);
					// Release structure
					SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
					return;
				}
				oc.status = OCS_PROXY;
				oc.ft->connection = &oc;
				// Join proxy tunnel
				proxy_sendJoinTunnel(&oc, oc.ft->wRemotePort);
			}
			else // stage 2 failed (empty IP)
			{ // try stage 3, or send response error 0x06
				if (!CreateOscarProxyConnection(&oc)) {
					oft_sendFileResponse(oc.dwUin, oc.szUid, oc.ft, 0x06);
					// notify UI
					ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc.ft, 0);
					// Release structure
					SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
					return;
				}
			}
		}
		else if (oc.type == OCT_PROXY) { // stage 4
			if (!CreateOscarProxyConnection(&oc)) { // proxy connection failed, we are out of possibilities
				ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc.ft, 0);
				// notify the other side, that we failed
				oft_sendFileResponse(oc.dwUin, oc.szUid, oc.ft, 0x06);
				// Release structure
				SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
				return;
			}
		}
		else if (oc.type == OCT_PROXY_INIT) { // stage 1
			if (!CreateOscarProxyConnection(&oc)) { // We failed to init transfer, notify UI
				icq_LogMessage(LOG_ERROR, LPGEN("Failed to Initialize File Transfer. Unable to bind local port and File proxy unavailable."));
				// Release transfer
				SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
				return;
			}
			else
				oc.type = OCT_PROXY_INIT;
		}
	}
	
	if (!oc.hConnection) { // one more sanity check
		NetLog_Direct("Error: No OFT connection.");
		return;
	}
	
	if (oc.status != OCS_PROXY) { // Connected, notify FT UI
		ProtoBroadcastAck(oc.ft->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, oc.ft, 0);

		// send init OFT frame - just for different order of packets (just like Trillian)
		if (oc.status == OCS_CONNECTED && (oc.ft->flags & OFTF_SENDING) && ((oc.ft->flags & OFTF_INITIALIZED) || oc.type == OCT_REVERSE) && !(oc.ft->flags & OFTF_FILE_REQUEST_SENT)) {
			oc.ft->flags |= OFTF_FILE_REQUEST_SENT;
			// proceed with first file
			oft_sendPeerInit(&oc);
		}
	}

	HANDLE hPacketRecver = Netlib_CreatePacketReceiver(oc.hConnection, 8192);
	NETLIBPACKETRECVER packetRecv = {};

	// Packet receiving loop

	while (oc.hConnection) {
		packetRecv.dwTimeout = oc.wantIdleTime ? 0 : 120000;

		int recvResult = Netlib_GetMorePackets(hPacketRecver, &packetRecv);
		if (!recvResult) {
			NetLog_Direct("Clean closure of oscar socket (%p)", oc.hConnection);
			break;
		}

		if (recvResult == SOCKET_ERROR) {
			if (GetLastError() == ERROR_TIMEOUT) { // TODO: this will not work on some systems
				if (oc.wantIdleTime) { // here we want to send file data packets
					oft_sendFileData(&oc);
				}
				else if (oc.status != OCS_WAITING) {
					NetLog_Direct("Connection timeouted, closing.");
					break;
				}
			}
			else if (oc.type != OCT_CLOSING || GetLastError() != 87) { // log only significant errors, not "connection killed by us"
				NetLog_Direct("Abortive closure of oscar socket (%p) (%d)", oc.hConnection, GetLastError());
				break;
			}
		}

		if (oc.type == OCT_CLOSING)
			packetRecv.bytesUsed = packetRecv.bytesAvailable;
		else
			packetRecv.bytesUsed = oft_handlePackets(&oc, packetRecv.buffer, packetRecv.bytesAvailable);
	}

	// End of packet receiving loop

	NetLib_SafeCloseHandle(&hPacketRecver);

	CloseOscarConnection(&oc);

	// Clean up
	{
		mir_cslock l(oftMutex);
		if (m_arFileTransfers.indexOf(oc.ft) != -1)
			oc.ft->connection = NULL; // release link
	}
	// Give server some time for abort/cancel to arrive
	SleepEx(1000, TRUE);
	// Error handling
	if (IsValidOscarTransfer(oc.ft)) {
		if (oc.status == OCS_DATA) {
			ProtoBroadcastAck(oc.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc.ft, 0);

			icq_LogMessage(LOG_ERROR, LPGEN("Connection lost during file transfer."));
			// Release structure
			SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
		}
		else if (oc.status == OCS_NEGOTIATION) {
			ProtoBroadcastAck(oc.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc.ft, 0);

			icq_LogMessage(LOG_ERROR, LPGEN("File transfer negotiation failed for unknown reason."));
			// Release structure
			SafeReleaseFileTransfer((basic_filetransfer**)&oc.ft);
		}
	}
}

void CIcqProto::sendOscarPacket(oscar_connection *oc, icq_packet *packet)
{
	if (oc->hConnection) {
		int nResult = Netlib_Send(oc->hConnection, (const char*)packet->pData, packet->wLen, 0);
		if (nResult == SOCKET_ERROR) {
			NetLog_Direct("Oscar %p socket error: %d, closing", oc->hConnection, GetLastError());
			CloseOscarConnection(oc);
		}
	}

	SAFE_FREE((void**)&packet->pData);
}

int CIcqProto::oft_handlePackets(oscar_connection *oc, BYTE *buf, size_t len)
{
	size_t bytesUsed = 0;

	while (len > 0) {
		if (oc->status == OCS_DATA && (oc->ft->flags & OFTF_FILE_RECEIVING))
			return oft_handleFileData(oc, buf, len);

		if (oc->status == OCS_PROXY)
			return oft_handleProxyData(oc, buf, len);

		if (len < 6)
			break;

		BYTE *pBuf = buf;
		DWORD dwHead;
		unpackDWord(&pBuf, &dwHead);
		if (dwHead != 0x4F465432) { // bad packet
			NetLog_Direct("OFT: Received invalid packet (dwHead = 0x%x).", dwHead);

			CloseOscarConnection(oc);
			break;
		}

		size_t datalen;
		unpackWord(&pBuf, &datalen);

		if (len < datalen) // wait for whole packet
			break;

		WORD datatype;
		unpackWord(&pBuf, &datatype);
#ifdef _DEBUG
		NetLog_Direct("OFT2: Type %u, Length %u bytes", datatype, datalen);
#endif
		handleOFT2FramePacket(oc, datatype, pBuf, datalen-8);

		/* Increase pointers so we can check for more data */
		buf += datalen;
		len -= datalen;
		bytesUsed += datalen;
	}

	return (int)bytesUsed;
}

int CIcqProto::oft_handleProxyData(oscar_connection *oc, BYTE *buf, size_t len)
{
	oscar_filetransfer *ft = oc->ft;
	size_t datalen, bytesUsed = 0;

	while (len > 2) {
		BYTE *pBuf = buf;

		unpackWord(&pBuf, &datalen);
		datalen += 2;

		if (len < datalen)
			break; // packet is not complete

		if (datalen < 12) { // malformed packet
			CloseOscarConnection(oc);
			break;
		}
		pBuf += 2; // packet version

		WORD wCommand;
		unpackWord(&pBuf, &wCommand);
		pBuf += 6;

		// handle packet
		switch (wCommand) {
		case 0x01: // Error
			WORD wError;
			char* szError;

			unpackWord(&pBuf, &wError);
			switch (wError) {
			case 0x0D:
				szError = "Bad request";
				break;
			case 0x0E:
				szError = "Malformed packet";
				break;
			case 0x10:
				szError = "Initial request timeout";
				break;
			case 0x1A:
				szError = "Accept period timeout";
				break;
			case 0x1C:
				szError = "Invalid data";
				break;

			default:
				szError = "Unknown";
			}
			// Notify peer
			oft_sendFileResponse(oc->dwUin, oc->szUid, oc->ft, 0x06);

			debugLogA("Proxy Error: %s (0x%x)", szError, wError);
			// Notify UI
			ProtoBroadcastAck(oc->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, oc->ft, 0);
			// Release structure
			SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
			break;

		case 0x03: // Tunnel created
			WORD wCode;
			DWORD dwIP;

			unpackWord(&pBuf, &wCode);
			unpackDWord(&pBuf, &dwIP);

			if (oc->type == OCT_PROXY_INIT) { // Proxy ready, send Stage 1 Request
				ft->bUseProxy = 1;
				ft->wRemotePort = wCode;
				ft->dwProxyIP = dwIP;
				oft_sendFileRequest(oc->dwUin, oc->szUid, ft, ft->szThisFile, 0);
				SAFE_FREE(&ft->szThisFile);
				// Notify UI
				ProtoBroadcastAck(oc->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, oc->ft, 0);
			}
			else {
				debugLogA("Proxy Tunnel ready, notify peer.");
				oft_sendFileRedirect(oc->dwUin, oc->szUid, ft, dwIP, wCode, TRUE);
			}
			break;

		case 0x05: // Connection ready
			oc->status = OCS_CONNECTED; // connection ready to send packets
			// Notify UI
			ProtoBroadcastAck(oc->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, oc->ft, 0);
			// signal we are ready
			if (oc->type == OCT_PROXY_RECV) {
				oft_sendFileAccept(oc->dwUin, oc->szUid, ft);
				if (ft->flags & OFTF_SENDING) // connection is ready for transfer (sending only)
					ft->flags |= OFTF_INITIALIZED;
			}

			debugLogA("Proxy Tunnel established");

			if ((ft->flags & OFTF_INITIALIZED) && (ft->flags & OFTF_SENDING) && !(ft->flags & OFTF_FILE_REQUEST_SENT)) {
				ft->flags |= OFTF_FILE_REQUEST_SENT;
				// proceed with first file
				oft_sendPeerInit(ft->connection);
			}
			break;

		default:
			debugLogA("Unknown proxy command 0x%x", wCommand);
		}

		buf += datalen;
		len -= datalen;
		bytesUsed += datalen;
	}

	return (int)bytesUsed;
}

int CIcqProto::oft_handleFileData(oscar_connection *oc, BYTE *buf, size_t len)
{
	oscar_filetransfer *ft = oc->ft;
	size_t dwLen = len, bytesUsed = 0;

	// do not accept more data than expected
	if (ft->qwThisFileSize - ft->qwFileBytesDone < dwLen)
		dwLen = (int)(ft->qwThisFileSize - ft->qwFileBytesDone);

	if (ft->fileId == -1) { // something went terribly bad
		CloseOscarConnection(oc);
		return 0;
	}

	_write(ft->fileId, buf, (unsigned)dwLen);
	// update checksum
	ft->dwRecvFileCheck = oft_calc_checksum((int)ft->qwFileBytesDone, buf, (unsigned)dwLen, ft->dwRecvFileCheck);
	bytesUsed += dwLen;
	ft->qwBytesDone += dwLen;
	ft->qwFileBytesDone += dwLen;

	if (GetTickCount() > ft->dwLastNotify + 700 || ft->qwFileBytesDone == ft->qwThisFileSize) { // notify FT UI of our progress, at most every 700ms - do not be faster than Miranda
		PROTOFILETRANSFERSTATUS pfts;

		oft_buildProtoFileTransferStatus(ft, &pfts);
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&pfts);
		ft->dwLastNotify = GetTickCount();
	}

	if (ft->qwFileBytesDone == ft->qwThisFileSize) {
		/* EOF */
		ft->flags &= ~OFTF_FILE_RECEIVING;

		_close(ft->fileId);
		ft->fileId = -1;

		if (ft->resumeAction != FILERESUME_SKIP && ft->dwRecvFileCheck != ft->dwThisFileCheck) {
			NetLog_Direct("Error: File checksums does not match!");

			// Notify UI
			char *pszMsg = ICQTranslateUtf(LPGEN("The checksum of file \"%s\" does not match, the file is probably damaged."));
			char szBuf[MAX_PATH];

			mir_snprintf(szBuf, pszMsg, ExtractFileName(ft->szThisFile));
			icq_LogMessage(LOG_ERROR, szBuf);

			SAFE_FREE(&pszMsg);
		}
		// keep transfer going (icq6 ignores checksums completely)
		else if (ft->resumeAction == FILERESUME_SKIP)
			NetLog_Direct("OFT: File receive skipped.");
		else
			NetLog_Direct("OFT: File received successfully.");

		if ((DWORD)(ft->iCurrentFile + 1) == ft->wFilesCount) {
			ft->bHeaderFlags = 0x01; // the whole process is over
			// ack received file
			sendOFT2FramePacket(oc, OFT_TYPE_DONE);
			oc->type = OCT_CLOSING;
			NetLog_Direct("File Transfer completed successfully.");
			ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft, 0);
			// Release transfer
			SafeReleaseFileTransfer((basic_filetransfer**)&ft);
		}
		else { // ack received file
			sendOFT2FramePacket(oc, OFT_TYPE_DONE);
			oc->status = OCS_NEGOTIATION;
		}
	}
	return (int)bytesUsed;
}

void CIcqProto::handleOFT2FramePacket(oscar_connection *oc, WORD datatype, BYTE *pBuffer, size_t wLen)
{
	oscar_filetransfer *ft = oc->ft;
	DWORD dwID1;
	DWORD dwID2;

	if (wLen < 232) { // allow shorter packets, but at least with filename
		NetLog_Direct("Error: Malformed OFT2 Frame, ignoring.");
		return;
	}

	unpackLEDWord(&pBuffer, &dwID1);
	wLen -= 4;
	unpackLEDWord(&pBuffer, &dwID2);
	wLen -= 4;

	if (datatype == OFT_TYPE_REQUEST && !(ft->flags & OFTF_FILE_REQUEST_RECEIVED)) { // first request does not contain MsgIDs we need to send them in ready packet
		dwID1 = ft->pMessage.dwMsgID1;
		dwID2 = ft->pMessage.dwMsgID2;
	}

	if (ft->pMessage.dwMsgID1 != dwID1 || ft->pMessage.dwMsgID2 != dwID2) { // this is not the right packet - bad Message IDs
		NetLog_Direct("Error: Invalid Packet Cookie, closing.");
		CloseOscarConnection(oc);
		return;
	}

	switch (datatype) {

	case OFT_TYPE_REQUEST:
	{ // Sender ready
		if (ft->flags & OFTF_SENDING) { // just sanity check - this is only for receiving client
			NetLog_Direct("Error: Invalid Packet, closing.");
			CloseOscarConnection(oc);
			return;
		}

		// Read Frame data
		if (!(ft->flags & OFTF_FILE_REQUEST_RECEIVED)) {
			unpackWord(&pBuffer, &ft->wEncrypt);
			unpackWord(&pBuffer, &ft->wCompress);
			unpackWord(&pBuffer, &ft->wFilesCount);
		}
		else
			pBuffer += 6;
		unpackWord(&pBuffer, &ft->wFilesLeft);
		ft->iCurrentFile = ft->wFilesCount - ft->wFilesLeft;
		if (!(ft->flags & OFTF_FILE_REQUEST_RECEIVED))
			unpackWord(&pBuffer, &ft->wPartsCount);
		else
			pBuffer += 2;
		unpackWord(&pBuffer, &ft->wPartsLeft);
		if (!(ft->flags & OFTF_FILE_REQUEST_RECEIVED)) { // just check it
			DWORD dwSize;

			unpackDWord(&pBuffer, &dwSize);
			if (dwSize != (DWORD)ft->qwTotalSize) { // the 32bits does not match, use them as full size
				ft->qwTotalSize = dwSize;
				debugLogA("Warning: Invalid total size.");
			}
		}
		else pBuffer += 4;

		// this allows us to receive single >4GB file correctly
		DWORD dwSize;
		unpackDWord(&pBuffer, &dwSize);
		if (dwSize == (DWORD)ft->qwTotalSize && ft->wFilesCount == 1)
			ft->qwThisFileSize = ft->qwTotalSize;
		else
			ft->qwThisFileSize = dwSize;

		unpackDWord(&pBuffer, &ft->dwThisFileDate);
		unpackDWord(&pBuffer, &ft->dwThisFileCheck);
		unpackDWord(&pBuffer, &ft->dwRecvForkCheck);
		unpackDWord(&pBuffer, &ft->dwThisForkSize);
		unpackDWord(&pBuffer, &ft->dwThisFileCreation);
		unpackDWord(&pBuffer, &ft->dwThisForkCheck);
		pBuffer += 4; // File Bytes Done
		unpackDWord(&pBuffer, &ft->dwRecvFileCheck);
		if (!(ft->flags & OFTF_FILE_REQUEST_RECEIVED))
			unpackString(&pBuffer, ft->rawIDString, 32);
		else
			pBuffer += 32;
		unpackByte(&pBuffer, &ft->bHeaderFlags);
		unpackByte(&pBuffer, &ft->bNameOff);
		unpackByte(&pBuffer, &ft->bSizeOff);
		if (!(ft->flags & OFTF_FILE_REQUEST_RECEIVED)) {
			unpackString(&pBuffer, (char*)ft->rawDummy, 69);
			unpackString(&pBuffer, (char*)ft->rawMacInfo, 16);
		}
		else pBuffer += 85;

		unpackWord(&pBuffer, &ft->wEncoding);
		unpackWord(&pBuffer, &ft->wSubEncoding);
		ft->cbRawFileName = wLen - 176;
		SAFE_FREE((void**)&ft->rawFileName); // release previous buffers
		SAFE_FREE(&ft->szThisFile);
		ft->rawFileName = (char*)SAFE_MALLOC(ft->cbRawFileName + 2);
		unpackString(&pBuffer, ft->rawFileName, ft->cbRawFileName);
		// Prepare file
		if (ft->wEncoding == 2) // UCS-2 encoding
			ft->szThisFile = ApplyEncoding(ft->rawFileName, "unicode-2-0");
		else
			ft->szThisFile = ansi_to_utf8(ft->rawFileName);

		// convert dir markings to normal backslashes
		for (size_t i = 0; i < mir_strlen(ft->szThisFile); i++)
			if (ft->szThisFile[i] == 0x01)
				ft->szThisFile[i] = '\\';

		ft->flags |= OFTF_FILE_REQUEST_RECEIVED; // First Frame Processed

		NetLog_Direct("File '%s', %I64u Bytes", ft->szThisFile, ft->qwThisFileSize);

		{ // Prepare Path Information
			char *szFile = strrchr(ft->szThisFile, '\\');

			SAFE_FREE(&ft->szThisPath); // release previous path
			if (szFile) {
				ft->szThisPath = ft->szThisFile;
				szFile[0] = '\0'; // split that strings
				ft->szThisFile = null_strdup(szFile + 1);
				// no cheating with paths
				if (!IsValidRelativePath(ft->szThisPath)) {
					NetLog_Direct("Invalid path information");
					break;
				}
			}
			else ft->szThisPath = null_strdup("");
		}

		/* no cheating with paths */
		if (!IsValidRelativePath(ft->szThisFile)) {
			NetLog_Direct("Invalid path information");
			break;
		}
		
		char *szFullPath = (char*)SAFE_MALLOC(mir_strlen(ft->szSavePath) + mir_strlen(ft->szThisPath) + mir_strlen(ft->szThisFile) + 3);
		mir_strcpy(szFullPath, ft->szSavePath);
		NormalizeBackslash(szFullPath);
		mir_strcat(szFullPath, ft->szThisPath);
		NormalizeBackslash(szFullPath);
		// make sure the dest dir exists
		if (MakeDirUtf(szFullPath))
			NetLog_Direct("Failed to create destination directory!");

		mir_strcat(szFullPath, ft->szThisFile);
		// we joined the full path to dest file
		SAFE_FREE(&ft->szThisFile);
		ft->szThisFile = szFullPath;

		ft->qwFileBytesDone = 0;

		{
			/* file resume */
			PROTOFILETRANSFERSTATUS pfts;

			oft_buildProtoFileTransferStatus(ft, &pfts);
			if (ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&pfts)) {
				oc->status = OCS_WAITING;
				break; /* UI supports resume: it will call PS_FILERESUME */
			}

			ft->fileId = OpenFileUtf(ft->szThisFile, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE);
#ifdef _DEBUG
			NetLog_Direct("OFT: OpenFileUtf(%s, %u) returned %u", ft->szThisFile, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, ft->fileId);
#endif
			if (ft->fileId == -1) {
#ifdef _DEBUG
				NetLog_Direct("OFT: errno=%d", errno);
#endif
				icq_LogMessage(LOG_ERROR, LPGEN("Your file receive has been aborted because Miranda could not open the destination file in order to write to it. You may be trying to save to a read-only folder."));

				ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
				// Release transfer
				SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
				return;
			}
		}
		// Send "we are ready"
		oc->status = OCS_DATA;
		ft->flags |= OFTF_FILE_RECEIVING;

		sendOFT2FramePacket(oc, OFT_TYPE_READY);
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);
		if (!ft->qwThisFileSize) { // if the file is empty we will not receive any data
			BYTE buf;
			oft_handleFileData(oc, &buf, 0);
		}
		return;
	}

	case OFT_TYPE_READY:
	case OFT_TYPE_RESUMEACK:
		// Receiver is ready
		oc->status = OCS_DATA;
		oc->wantIdleTime = 1;
		ft->flags |= OFTF_FILE_SENDING;

		NetLog_Direct("OFT: Receiver ready.");
		break;

	case OFT_TYPE_RESUMEREQUEST:
		// Receiver wants to resume file transfer from point
		if (!(ft->flags & OFTF_SENDING)) { // just sanity check - this is only for sending client
			NetLog_Direct("Error: Invalid Packet, closing.");
			CloseOscarConnection(oc);
			return;
		}
		{
			DWORD dwResumeCheck, dwResumeOffset, dwFileCheck;
			// Read Resume Frame data
			pBuffer += 44;
			unpackDWord(&pBuffer, &dwResumeOffset);
			unpackDWord(&pBuffer, &dwResumeCheck);

			dwFileCheck = oft_calc_file_checksum(ft->fileId, dwResumeOffset);
			if (dwFileCheck == dwResumeCheck && dwResumeOffset <= ft->qwThisFileSize) { // resume seems ok
				ft->qwFileBytesDone = dwResumeOffset;
				ft->qwBytesDone += dwResumeOffset;
				_lseek(ft->fileId, dwResumeOffset, SEEK_SET);

				NetLog_Direct("OFT: Resume request, ready.");
			}
			else
				NetLog_Direct("OFT: Resume request, restarting.");

			// Ready for resume
			sendOFT2FramePacket(oc, OFT_TYPE_RESUMEREADY);
		}
		break;

	case OFT_TYPE_RESUMEREADY:
		// Process Smart-resume reply
		if (ft->flags & OFTF_SENDING) { // just sanity check - this is only for receiving client
			NetLog_Direct("Error: Invalid Packet, closing.");
			CloseOscarConnection(oc);
			return;
		}
		{
			DWORD dwResumeOffset, dwResumeCheck;
			// Read Resume Reply data
			pBuffer += 44;
			unpackDWord(&pBuffer, &dwResumeOffset);
			unpackDWord(&pBuffer, &dwResumeCheck);

			if (ft->qwFileBytesDone != dwResumeOffset) {
				ft->qwBytesDone -= (ft->qwFileBytesDone - dwResumeOffset);
				ft->qwFileBytesDone = dwResumeOffset;
				if (dwResumeOffset)
					ft->dwRecvFileCheck = dwResumeCheck;
				else // Restarted resume (data mismatch)
					ft->dwRecvFileCheck = 0xFFFF0000;
			}
			_lseek(ft->fileId, dwResumeOffset, SEEK_SET);

			if (ft->qwThisFileSize != ft->qwFileBytesDone)
				NetLog_Direct("OFT: Resuming from offset %u.", dwResumeOffset);

			// Prepare to receive data
			oc->status = OCS_DATA;

			ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);

			// Ready for receive
			sendOFT2FramePacket(oc, OFT_TYPE_RESUMEACK);

			if (ft->qwThisFileSize == ft->qwFileBytesDone) { // all data already processed
				BYTE buf;

				oft_handleFileData(oc, &buf, 0);
			}
		}
		break;

	case OFT_TYPE_DONE:
		// File done
		oc->status = OCS_NEGOTIATION;
		oc->wantIdleTime = 0;

		ft->flags &= ~OFTF_FILE_SENDING;

		NetLog_Direct("OFT: File sent successfully.");

#ifdef _DEBUG
		NetLog_Direct("OFT: _close(%u)", ft->fileId);
#endif
		_close(ft->fileId); // FIXME: this needs fix for "skip file" feature
		ft->fileId = -1;
		ft->iCurrentFile++;
		// continue with next file
		oft_sendPeerInit(oc);
		break;

	default:
		NetLog_Direct("Error: Uknown OFT frame type 0x%x", datatype);
	}
}

//
// Proxy packets
/////////////////////////////

void CIcqProto::proxy_sendInitTunnel(oscar_connection *oc)
{
	icq_packet packet;
	size_t wLen = 39 + getUINLen(m_dwLocalUIN);

	packet.wLen = WORD(wLen);
	init_generic_packet(&packet, 2);

	packWord(&packet, WORD(wLen));
	packWord(&packet, OSCAR_PROXY_VERSION);
	packWord(&packet, 0x02);                      // wCommand
	packDWord(&packet, 0);                        // Unknown
	packWord(&packet, 0);                         // Flags?
	packUIN(&packet, m_dwLocalUIN);
	packLEDWord(&packet, oc->ft->pMessage.dwMsgID1);
	packLEDWord(&packet, oc->ft->pMessage.dwMsgID2);
	packDWord(&packet, 0x00010010);               // TLV(1)
	packGUID(&packet, MCAP_FILE_TRANSFER);

	sendOscarPacket(oc, &packet);
}

void CIcqProto::proxy_sendJoinTunnel(oscar_connection *oc, WORD wPort)
{
	icq_packet packet;
	size_t wLen = 41 + getUINLen(m_dwLocalUIN);

	packet.wLen = WORD(wLen);
	init_generic_packet(&packet, 2);

	packWord(&packet, WORD(wLen));
	packWord(&packet, OSCAR_PROXY_VERSION);
	packWord(&packet, 0x04);                      // wCommand
	packDWord(&packet, 0);                        // Unknown
	packWord(&packet, 0);                         // Flags?
	packUIN(&packet, m_dwLocalUIN);
	packWord(&packet, wPort);
	packLEDWord(&packet, oc->ft->pMessage.dwMsgID1);
	packLEDWord(&packet, oc->ft->pMessage.dwMsgID2);
	packDWord(&packet, 0x00010010);               // TLV(1)
	packGUID(&packet, MCAP_FILE_TRANSFER);

	sendOscarPacket(oc, &packet);
}

//
// Direct packets
/////////////////////////////

void CIcqProto::oft_sendFileData(oscar_connection *oc)
{
	oscar_filetransfer *ft = oc->ft;
	BYTE buf[OFT_BUFFER_SIZE];

	if (ft->fileId == -1)
		return;

	int bytesRead = _read(ft->fileId, buf, sizeof(buf));
	if (bytesRead == -1)
		return;

	if (!bytesRead) { //
		oc->wantIdleTime = 0;
		return;
	}

	if ((DWORD)bytesRead > (ft->qwThisFileSize - ft->qwFileBytesDone)) { // do not send more than expected, limit to known size
		bytesRead = (DWORD)(ft->qwThisFileSize - ft->qwFileBytesDone);
		oc->wantIdleTime = 0;
	}

	if (bytesRead) {
		icq_packet packet;
		packet.wLen = bytesRead;
		init_generic_packet(&packet, 0);
		packBuffer(&packet, buf, bytesRead); // we are sending raw data
		sendOscarPacket(oc, &packet);

		ft->qwBytesDone += bytesRead;
		ft->qwFileBytesDone += bytesRead;
	}

	if (GetTickCount() > ft->dwLastNotify + 700 || oc->wantIdleTime == 0 || ft->qwFileBytesDone == ft->qwThisFileSize) { // notify only once a while or after last data packet sent
		PROTOFILETRANSFERSTATUS pfts;
		oft_buildProtoFileTransferStatus(ft, &pfts);
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&pfts);
		ft->dwLastNotify = GetTickCount();
	}
}

void CIcqProto::oft_sendPeerInit(oscar_connection *oc)
{
	mir_cslock l(oftMutex);

	// prepare init frame
	oscar_filetransfer *ft = oc->ft;
	if (ft->iCurrentFile >= (int)ft->wFilesCount) { // All files done, great!
		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft, 0);
		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
		return;
	}

	SAFE_FREE(&ft->szThisFile);
	ft->szThisFile = null_strdup(ft->files[ft->iCurrentFile].szFile);

	struct _stati64 statbuf;
	if (FileStatUtf(ft->szThisFile, &statbuf)) {
		icq_LogMessage(LOG_ERROR, LPGEN("Your file transfer has been aborted because one of the files that you selected to send is no longer readable from the disk. You may have deleted or moved it."));

		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
		return;
	}

	// create full relative filename
	char* szThisContainer = ft->files[ft->iCurrentFile].szContainer;

	char *pszThisFileName = (char*)SAFE_MALLOC(mir_strlen(ft->szThisFile) + mir_strlen(szThisContainer) + 4);
	mir_strcpy(pszThisFileName, szThisContainer);
	NormalizeBackslash(pszThisFileName);
	mir_strcat(pszThisFileName, ExtractFileName(ft->szThisFile));
	
	// convert backslashes to dir markings
	for (size_t i = 0; i < mir_strlen(pszThisFileName); i++)
		if (pszThisFileName[i] == '\\' || pszThisFileName[i] == '/')
			pszThisFileName[i] = 0x01;

	ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0);

	ft->fileId = OpenFileUtf(ft->szThisFile, _O_BINARY | _O_RDONLY, 0);
	if (ft->fileId == -1) {
		NetLog_Direct("OFT: error opening file %s, errno=%d", ft->szThisFile, errno);
		SAFE_FREE((void**)&pszThisFileName);
		icq_LogMessage(LOG_ERROR, LPGEN("Your file transfer has been aborted (error=%d), because one of the files that you selected to send is no longer readable from the disk. You may have deleted or moved it."));

		ProtoBroadcastAck(ft->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0);
		// Release transfer
		SafeReleaseFileTransfer((basic_filetransfer**)&oc->ft);
		return;
	}

	ft->qwThisFileSize = statbuf.st_size;
	ft->dwThisFileDate = statbuf.st_mtime;
	ft->dwThisFileCreation = statbuf.st_ctime;
	ft->dwThisFileCheck = oft_calc_file_checksum(ft->fileId, ft->qwThisFileSize);
	ft->qwFileBytesDone = 0;
	ft->dwRecvFileCheck = 0xFFFF0000;
	SAFE_FREE((void**)&ft->rawFileName);

	if (IsUSASCII(pszThisFileName, mir_strlen(pszThisFileName))) {
		ft->wEncoding = 0; // ascii
		ft->cbRawFileName = WORD(mir_strlen(pszThisFileName) + 1);
		if (ft->cbRawFileName < 64)
			ft->cbRawFileName = 64;
		ft->rawFileName = (char*)SAFE_MALLOC(ft->cbRawFileName);
		mir_strcpy(ft->rawFileName, (char*)pszThisFileName);
		SAFE_FREE((void**)&pszThisFileName);
	}
	else {
		ft->wEncoding = 2; // ucs-2
		WCHAR *pwsThisFile = make_unicode_string(pszThisFileName);
		SAFE_FREE((void**)&pszThisFileName);
		ft->cbRawFileName = WORD(mir_wstrlen(pwsThisFile) * sizeof(WCHAR)) + 2;
		if (ft->cbRawFileName < 64)
			ft->cbRawFileName = 64;
		ft->rawFileName = (char*)SAFE_MALLOC(ft->cbRawFileName);
		// convert to LE ordered string
		BYTE *pwsThisFileBuf = (BYTE*)pwsThisFile; // need this - unpackWideString moves the address!
		unpackWideString(&pwsThisFileBuf, (WCHAR*)ft->rawFileName, mir_wstrlen(pwsThisFile) * sizeof(WCHAR));
		SAFE_FREE((void**)&pwsThisFile);
	}
	ft->wFilesLeft = WORD(ft->wFilesCount - ft->iCurrentFile);

	sendOFT2FramePacket(oc, OFT_TYPE_REQUEST);
}

void CIcqProto::sendOFT2FramePacket(oscar_connection *oc, WORD datatype)
{
	oscar_filetransfer *ft = oc->ft;

	icq_packet packet;
	packet.wLen = WORD(192 + ft->cbRawFileName);
	init_generic_packet(&packet, 0);

	// Basic Oscar Frame
	packDWord(&packet, 0x4F465432); // Magic
	packWord(&packet, packet.wLen);
	packWord(&packet, datatype);

	// Cookie
	packLEDWord(&packet, ft->pMessage.dwMsgID1);
	packLEDWord(&packet, ft->pMessage.dwMsgID2);
	packWord(&packet, ft->wEncrypt);
	packWord(&packet, ft->wCompress);
	packWord(&packet, ft->wFilesCount);
	packWord(&packet, ft->wFilesLeft);
	packWord(&packet, ft->wPartsCount);
	packWord(&packet, ft->wPartsLeft);
	packDWord(&packet, (DWORD)ft->qwTotalSize);
	packDWord(&packet, (DWORD)ft->qwThisFileSize);
	packDWord(&packet, ft->dwThisFileDate);
	packDWord(&packet, ft->dwThisFileCheck);
	packDWord(&packet, ft->dwRecvForkCheck);
	packDWord(&packet, ft->dwThisForkSize);
	packDWord(&packet, ft->dwThisFileCreation);
	packDWord(&packet, ft->dwThisForkCheck);
	packDWord(&packet, (DWORD)ft->qwFileBytesDone);
	packDWord(&packet, ft->dwRecvFileCheck);
	packBuffer(&packet, (LPBYTE)ft->rawIDString, 32);
	packByte(&packet, ft->bHeaderFlags);
	packByte(&packet, ft->bNameOff);
	packByte(&packet, ft->bSizeOff);
	packBuffer(&packet, ft->rawDummy, 69);
	packBuffer(&packet, ft->rawMacInfo, 16);
	packWord(&packet, ft->wEncoding);
	packWord(&packet, ft->wSubEncoding);
	packBuffer(&packet, (LPBYTE)ft->rawFileName, ft->cbRawFileName);

	sendOscarPacket(oc, &packet);
}