/*

Jabber Protocol Plugin for Miranda IM
Copyright ( C ) 2002-04  Santithorn Bunchua
Copyright ( C ) 2005-11  George Hazan

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.

Revision       : $Revision: 13455 $
Last change on : $Date: 2011-03-17 15:32:29 -0400 (Thu, 17 Mar 2011) $
Last change by : $Author: george.hazan $

*/

#include "jabber.h"
#include <io.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "jabber_caps.h"

#define JABBER_NETWORK_BUFFER_SIZE 2048

void __cdecl CJabberProto::FileReceiveThread( filetransfer* ft )
{
	char* buffer;
	int datalen;
	ThreadData info( this, JABBER_SESSION_NORMAL );

	Log( "Thread started: type=file_receive server='%s' port='%d'", ft->httpHostName, ft->httpPort );

	ft->type = FT_OOB;

	if (( buffer=( char* )mir_alloc( JABBER_NETWORK_BUFFER_SIZE )) == NULL ) {
		Log( "Cannot allocate network buffer, thread ended" );
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0 );
		delete ft;
		return;
	}

	NETLIBOPENCONNECTION nloc = { 0 };
	nloc.cbSize = sizeof( nloc );
	nloc.cbSize = sizeof( NETLIBOPENCONNECTION );
	nloc.szHost = ft->httpHostName;
	nloc.wPort = ft->httpPort;
	info.s = ( HANDLE ) JCallService( MS_NETLIB_OPENCONNECTION, ( WPARAM ) m_hNetlibUser, ( LPARAM )&nloc );
	if ( info.s == NULL ) {
		Log( "Connection failed ( %d ), thread ended", WSAGetLastError());
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0 );
		mir_free( buffer );
		delete ft;
		return;
	}

	ft->s = info.s;

	info.send( "GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", ft->httpPath, ft->httpHostName );
	ft->state = FT_CONNECTING;

	Log( "Entering file_receive recv loop" );
	datalen = 0;

	while ( ft->state != FT_DONE && ft->state != FT_ERROR ) {
		int recvResult, bytesParsed;

		Log( "Waiting for data..." );
		recvResult = info.recv( buffer+datalen, JABBER_NETWORK_BUFFER_SIZE-datalen );
		if ( recvResult <= 0 )
			break;
		datalen += recvResult;

		bytesParsed = FileReceiveParse( ft, buffer, datalen );
		if ( bytesParsed < datalen )
			memmove( buffer, buffer+bytesParsed, datalen-bytesParsed );
		datalen -= bytesParsed;
	}

	ft->s = NULL;

	if ( ft->state==FT_DONE || ( ft->state==FT_RECEIVING && ft->std.currentFileSize < 0 ))
		ft->complete();

	Log( "Thread ended: type=file_receive server='%s'", ft->httpHostName );

	mir_free( buffer );
	delete ft;
}

int CJabberProto::FileReceiveParse( filetransfer* ft, char* buffer, int datalen )
{
	char* p, *q, *s, *eob;
	char* str;
	int num, code;

	eob = buffer + datalen;
	p = buffer;
	num = 0;
	while ( true ) {
		if ( ft->state==FT_CONNECTING || ft->state==FT_INITIALIZING ) {
			for ( q=p; q+1<eob && ( *q!='\r' || *( q+1 )!='\n' ); q++ );
			if ( q+1 < eob ) {
				if (( str=( char* )mir_alloc( q-p+1 )) != NULL ) {
					strncpy( str, p, q-p );
					str[q-p] = '\0';
					Log( "FT Got: %s", str );
					if ( ft->state == FT_CONNECTING ) {
						// looking for "HTTP/1.1 200 OK"
						if ( sscanf( str, "HTTP/%*d.%*d %d %*s", &code )==1 && code==200 ) {
							ft->state = FT_INITIALIZING;
							ft->std.currentFileSize = -1;
							Log( "Change to FT_INITIALIZING" );
							JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, ft, 0 );
						}
					}
					else {	// FT_INITIALIZING
						if ( str[0] == '\0' ) {
							TCHAR* s;
							if (( s = _tcsrchr( ft->httpPath, '/' )) != NULL )
								s++;
							else
								s = ft->httpPath;
							ft->std.tszCurrentFile = mir_tstrdup( s );
							JabberHttpUrlDecode( ft->std.tszCurrentFile );
							if ( ft->create() == -1 ) {
								ft->state = FT_ERROR;
								break;
							}
							ft->state = FT_RECEIVING;
							ft->std.currentFileProgress = 0;
							Log( "Change to FT_RECEIVING" );
						}
						else if (( s=strchr( str, ':' )) != NULL ) {
							*s = '\0';
							if ( !strcmp( str, "Content-Length" ))
								ft->std.totalBytes = ft->std.currentFileSize = _atoi64( s+1 );
					}	}

					mir_free( str );
					q += 2;
					num += ( q-p );
					p = q;
				}
				else {
					ft->state = FT_ERROR;
					break;
				}
			}
			else {
				break;
			}
		}
		else if ( ft->state == FT_RECEIVING ) {
			int bufferSize, writeSize;
            __int64 remainingBytes;

			if ( ft->std.currentFileSize < 0 || ft->std.currentFileProgress < ft->std.currentFileSize ) {
				bufferSize = eob - p;
				remainingBytes = ft->std.currentFileSize - ft->std.currentFileProgress;
				if ( remainingBytes < bufferSize )
					writeSize = remainingBytes;
				else
					writeSize = bufferSize;
				if ( _write( ft->fileId, p, writeSize ) != writeSize ) {
					Log( "_write() error" );
					ft->state = FT_ERROR;
				}
				else {
					ft->std.currentFileProgress += writeSize;
					ft->std.totalProgress += writeSize;
					JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, ( LPARAM )&ft->std );
					if ( ft->std.currentFileProgress == ft->std.currentFileSize )
						ft->state = FT_DONE;
				}
			}
			num = datalen;
			break;
		}
		else break;
	}

	return num;
}

void JabberFileServerConnection( JABBER_SOCKET hConnection, DWORD /*dwRemoteIP*/, void* extra )
{
	CJabberProto* ppro = ( CJabberProto* )extra;
	WORD localPort = 0;
	SOCKET s = JCallService( MS_NETLIB_GETSOCKET, ( WPARAM ) hConnection, 0 ); 
	if ( s != INVALID_SOCKET ) {
		SOCKADDR_IN saddr;
		int len = sizeof( saddr );
		if ( getsockname( s, ( SOCKADDR * ) &saddr, &len ) != SOCKET_ERROR ) {
			localPort = ntohs( saddr.sin_port );
		}
	}
	if ( localPort == 0 ) {
		ppro->Log( "Unable to determine the local port, file server connection closed." );
		Netlib_CloseHandle( hConnection );
		return;
	}

	TCHAR szPort[20];
	mir_sntprintf( szPort, SIZEOF( szPort ), _T("%d"), localPort );
	ppro->Log( "File server incoming connection accepted: local_port=" TCHAR_STR_PARAM, szPort );

	JABBER_LIST_ITEM *item = ppro->ListGetItemPtr( LIST_FILE, szPort );
	if ( item == NULL ) {
		ppro->Log( "No file is currently served, file server connection closed." );
		Netlib_CloseHandle( hConnection );
		return;
	}

	filetransfer* ft = item->ft;
	JABBER_SOCKET slisten = ft->s;
	ft->s = hConnection;
	ppro->Log( "Set ft->s to %d ( saving %d )", hConnection, slisten );

	char* buffer = ( char* )mir_alloc( JABBER_NETWORK_BUFFER_SIZE+1 );
	if ( buffer == NULL ) {
		ppro->Log( "Cannot allocate network buffer, file server connection closed." );
		Netlib_CloseHandle( hConnection );
		ft->state = FT_ERROR;
		if ( ft->hFileEvent != NULL )
			SetEvent( ft->hFileEvent );
		return;
	}

	ppro->Log( "Entering recv loop for this file connection... ( ft->s is hConnection )" );
	int datalen = 0;
	while ( ft->state!=FT_DONE && ft->state!=FT_ERROR ) {
		int recvResult, bytesParsed;

		recvResult = Netlib_Recv( hConnection, buffer+datalen, JABBER_NETWORK_BUFFER_SIZE-datalen, 0 );
		if ( recvResult <= 0 )
			break;
		datalen += recvResult;

		buffer[datalen] = '\0';
		ppro->Log( "RECV:%s", buffer );

		bytesParsed = ppro->FileSendParse( hConnection, ft, buffer, datalen );
		if ( bytesParsed < datalen )
			memmove( buffer, buffer+bytesParsed, datalen-bytesParsed );
		datalen -= bytesParsed;
	}

	ppro->Log( "Closing connection for this file transfer... ( ft->s is now hBind )" );
	Netlib_CloseHandle( hConnection );
	ft->s = slisten;
	ppro->Log( "ft->s is restored to %d", ft->s );
	if ( ft->hFileEvent != NULL )
		SetEvent( ft->hFileEvent );
	mir_free( buffer );
}

void __cdecl CJabberProto::FileServerThread( filetransfer* ft )
{
	Log( "Thread started: type=file_send" );

	ThreadData info( this, JABBER_SESSION_NORMAL );
	ft->type = FT_OOB;

	NETLIBBIND nlb = {0};
	nlb.cbSize = sizeof( NETLIBBIND );
	nlb.pfnNewConnectionV2 = JabberFileServerConnection;
	nlb.pExtra = this;
	nlb.wPort = 0;	// Use user-specified incoming port ranges, if available
	info.s = ( HANDLE ) JCallService( MS_NETLIB_BINDPORT, ( WPARAM ) m_hNetlibUser, ( LPARAM )&nlb );
	if ( info.s == NULL ) {
		Log( "Cannot allocate port to bind for file server thread, thread ended." );
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0 );
		delete ft;
		return;
	}

	ft->s = info.s;
	Log( "ft->s = %d", info.s );

	HANDLE hEvent = CreateEvent( NULL, FALSE, FALSE, NULL );
	ft->hFileEvent = hEvent;

	TCHAR szPort[20];
	mir_sntprintf( szPort, SIZEOF( szPort ), _T("%d"), nlb.wPort );
	JABBER_LIST_ITEM *item = ListAdd( LIST_FILE, szPort );
	item->ft = ft;

	TCHAR* ptszResource = ListGetBestClientResourceNamePtr( ft->jid );
	if ( ptszResource != NULL ) {
		ft->state = FT_CONNECTING;
		for ( int i=0; i < ft->std.totalFiles && ft->state != FT_ERROR && ft->state != FT_DENIED; i++ ) {
			ft->std.currentFileNumber = i;
			ft->state = FT_CONNECTING;
			if ( ft->httpPath ) mir_free( ft->httpPath );
			ft->httpPath = NULL;

			TCHAR* p;
			if (( p = _tcschr( ft->std.ptszFiles[i], '\\' )) != NULL )
				p++;
			else
				p = ft->std.ptszFiles[i];

			in_addr in;
			in.S_un.S_addr = m_dwJabberLocalIP;
		
			TCHAR* pFileName = JabberHttpUrlEncode( p );
			if ( pFileName != NULL ) {
				int id = SerialNext();
				if ( ft->iqId ) mir_free( ft->iqId );
				ft->iqId = ( TCHAR* )mir_alloc( sizeof(TCHAR)*( strlen( JABBER_IQID )+20 ));
				wsprintf( ft->iqId, _T(JABBER_IQID)_T("%d"), id );

				char *myAddr;
				DBVARIANT dbv;
				if (m_options.BsDirect && m_options.BsDirectManual) {
					if ( !DBGetContactSettingString( NULL, m_szModuleName, "BsDirectAddr", &dbv )) {
						myAddr = NEWSTR_ALLOCA( dbv.pszVal );
						JFreeVariant( &dbv );
					}
					else myAddr = inet_ntoa( in );
				}
				else myAddr = inet_ntoa( in );

				char szAddr[ 256 ];
				mir_snprintf( szAddr, sizeof(szAddr), "http://%s:%d/%s", myAddr, nlb.wPort, pFileName );

				int len = lstrlen(ptszResource) + lstrlen(ft->jid) + 2;
				TCHAR* fulljid = ( TCHAR* )alloca( sizeof( TCHAR )*len );
				wsprintf( fulljid, _T("%s/%s"), ft->jid, ptszResource );

				XmlNodeIq iq( _T("set"), id, fulljid );
				HXML query = iq << XQUERY( _T(JABBER_FEAT_OOB));
				query << XCHILD( _T("url"), _A2T(szAddr));
				query << XCHILD( _T("desc"), ft->szDescription);
				m_ThreadInfo->send( iq );

				Log( "Waiting for the file to be sent..." );
				WaitForSingleObject( hEvent, INFINITE );
				mir_free( pFileName );
			}
			Log( "File sent, advancing to the next file..." );
			JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, ft, 0 );
		}
		CloseHandle( hEvent );
		ft->hFileEvent = NULL;
		Log( "Finish all files" );
	}

	ft->s = NULL;
	Log( "ft->s is NULL" );

	ListRemove( LIST_FILE, szPort );

	switch ( ft->state ) {
	case FT_DONE:
		Log( "Finish successfully" );
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft, 0 );
		break;
	case FT_DENIED:
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DENIED, ft, 0 );
		break;
	default: // FT_ERROR:
		Log( "Finish with errors" );
		JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, ft, 0 );
		break;
	}

	Log( "Thread ended: type=file_send" );
	delete ft;
}

int CJabberProto::FileSendParse( JABBER_SOCKET s, filetransfer* ft, char* buffer, int datalen )
{
	char* p, *q, *t, *eob;
	char* str;
	int num;
	int currentFile;
	int fileId;
	int numRead;

	eob = buffer + datalen;
	p = buffer;
	num = 0;
	while ( ft->state==FT_CONNECTING || ft->state==FT_INITIALIZING ) {
		for ( q=p; q+1<eob && ( *q!='\r' || *( q+1 )!='\n' ); q++ );
		if ( q+1 >= eob )
			break;
		if (( str=( char* )mir_alloc( q-p+1 )) == NULL ) {
			ft->state = FT_ERROR;
			break;
		}
		strncpy( str, p, q-p );
		str[q-p] = '\0';
		Log( "FT Got: %s", str );
		if ( ft->state == FT_CONNECTING ) {
			// looking for "GET filename.ext HTTP/1.1"
			if ( !strncmp( str, "GET ", 4 )) {
				for ( t=str+4; *t!='\0' && *t!=' '; t++ );
				*t = '\0';
				for ( t=str+4; *t!='\0' && *t=='/'; t++ );
				ft->httpPath = mir_a2t( t );
				JabberHttpUrlDecode( ft->httpPath );
				ft->state = FT_INITIALIZING;
				Log( "Change to FT_INITIALIZING" );
			}
		}
		else {	// FT_INITIALIZING
			if ( str[0] == '\0' ) {
				struct _stati64 statbuf;

				mir_free( str );
				num += 2;

				currentFile = ft->std.currentFileNumber;
				TCHAR* t = _tcsrchr( ft->std.ptszFiles[ currentFile ], '\\' ); 
				if ( t != NULL )
					t++;
				else
					t = ft->std.ptszFiles[currentFile];

				if ( ft->httpPath==NULL || lstrcmp( ft->httpPath, t )) {
					if ( ft->httpPath == NULL )
						Log( "Requested file name does not matched ( httpPath==NULL )" );
					else
						Log( "Requested file name does not matched ( '%s' vs. '%s' )", ft->httpPath, t );
					ft->state = FT_ERROR;
					break;
				}
				Log( "Sending [%s]", ft->std.ptszFiles[ currentFile ] );
				_tstati64( ft->std.ptszFiles[ currentFile ], &statbuf );	// file size in statbuf.st_size
				if (( fileId = _topen( ft->std.ptszFiles[currentFile], _O_BINARY|_O_RDONLY )) < 0 ) {
					Log( "File cannot be opened" );
					ft->state = FT_ERROR;
					mir_free( ft->httpPath );
					ft->httpPath = NULL;
					break;
				}

				char fileBuffer[ 2048 ];
				int bytes = mir_snprintf( fileBuffer, sizeof(fileBuffer), "HTTP/1.1 200 OK\r\nContent-Length: %I64u\r\n\r\n", statbuf.st_size );
				WsSend( s, fileBuffer, bytes, MSG_DUMPASTEXT );

				ft->std.flags |= PFTS_SENDING;
				ft->std.currentFileProgress = 0;
				Log( "Sending file data..." );

				while (( numRead = _read( fileId, fileBuffer, 2048 )) > 0 ) {
					if ( Netlib_Send( s, fileBuffer, numRead, 0 ) != numRead ) {
						ft->state = FT_ERROR;
						break;
					}
					ft->std.currentFileProgress += numRead;
					ft->std.totalProgress += numRead;
					JSendBroadcast( ft->std.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, ( LPARAM )&ft->std );
				}
				_close( fileId );
				if ( ft->state != FT_ERROR )
					ft->state = FT_DONE;
				Log( "Finishing this file..." );
				mir_free( ft->httpPath );
				ft->httpPath = NULL;
				break;
		}	}

		mir_free( str );
		q += 2;
		num += ( q-p );
		p = q;
	}

	return num;
}

/////////////////////////////////////////////////////////////////////////////////////////
// filetransfer class members

filetransfer::filetransfer( CJabberProto* proto )
{
	memset( this, 0, sizeof( filetransfer ));
	ppro = proto;
	fileId = -1;
	std.cbSize = sizeof( std );
	std.flags = PFTS_TCHAR;
}

filetransfer::~filetransfer()
{
	ppro->Log( "Destroying file transfer session %08p", this );

	if ( !bCompleted )
		ppro->JSendBroadcast( std.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, this, 0 );

	close();

	if ( hWaitEvent != INVALID_HANDLE_VALUE )
		CloseHandle( hWaitEvent );

	if ( jid ) mir_free( jid );
	if ( sid ) mir_free( sid );
	if ( iqId ) mir_free( iqId );
	if ( fileSize ) mir_free( fileSize );
	if ( httpHostName ) mir_free( httpHostName );
	if ( httpPath ) mir_free( httpPath );
	if ( szDescription ) mir_free( szDescription );

	if ( std.tszWorkingDir ) mir_free( std.tszWorkingDir );
	if ( std.tszCurrentFile ) mir_free( std.tszCurrentFile );

	if ( std.ptszFiles ) {
		for ( int i=0; i < std.totalFiles; i++ )
			if ( std.ptszFiles[i] ) mir_free( std.ptszFiles[i] );

		mir_free( std.ptszFiles );
}	}

void filetransfer::close()
{
	if ( fileId != -1 ) {
		_close( fileId );
		fileId = -1;
}	}

void filetransfer::complete()
{
	close();

	bCompleted = true;
	ppro->JSendBroadcast( std.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, this, 0);
}

int filetransfer::create()
{
	if ( fileId != -1 )
		return fileId;

	TCHAR filefull[ MAX_PATH ];
	mir_sntprintf( filefull, SIZEOF(filefull), _T("%s\\%s"), std.tszWorkingDir, std.tszCurrentFile );
	replaceStr( std.tszCurrentFile, filefull );

	if ( hWaitEvent != INVALID_HANDLE_VALUE )
		CloseHandle( hWaitEvent );
	hWaitEvent = CreateEvent( NULL, FALSE, FALSE, NULL );

	if ( ppro->JSendBroadcast( std.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, this, ( LPARAM )&std ))
		WaitForSingleObject( hWaitEvent, INFINITE );

	if ( fileId == -1 ) {
		ppro->Log( "Saving to [%s]", std.tszCurrentFile );
		fileId = _topen( std.tszCurrentFile, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE );
	}

	if ( fileId == -1 )
		ppro->Log( "Cannot create file '%s' during a file transfer", filefull );
	else if ( std.currentFileSize != 0 )
		_chsize( fileId, std.currentFileSize );

	return fileId;
}
