/*
GameSpy GHTTP SDK 
Dan "Mr. Pants" Schoenblum
dan@gamespy.com

Copyright 1999-2007 GameSpy Industries, Inc

devsupport@gamespy.com
*/

#include "ghttpProcess.h"
#include "ghttpCallbacks.h"
#include "ghttpPost.h"
#include "ghttpMain.h"
#include "ghttpCommon.h"

// Parse the URL into:
//   server address (and IP)
//   server port
//   request path.
/////////////////////////////
static GHTTPBool ghiParseURL
(
	GHIConnection * connection
)
{
	char * URL;
	int nIndex;
	char tempChar;
	char * str;

	assert(connection);
	if(!connection)
		return GHTTPFalse;

	// 2002.Apr.18.JED - Make sure we have an URL
	/////////////////////////////////////////////
	assert(connection->URL);
	if(!connection->URL)
		return GHTTPFalse;

	URL = connection->URL;

	// Check for "http://".
	//////////////////////
	if(strncmp(URL, "http://", 7) == 0)
	{
		connection->protocol = GHIHttp;
		URL += 7;
	}
	else if (strncmp(URL, "https://", 8) == 0)
	{
		connection->protocol = GHIHttps;
		URL += 8;
	}
	else
	{
		return GHTTPFalse;
	}

	// Read the address.
	////////////////////
	nIndex = (int)strcspn(URL, ":/");
	tempChar = URL[nIndex];
	URL[nIndex] = '\0';
	connection->serverAddress = goastrdup(URL);
	if(!connection->serverAddress)
		return GHTTPFalse;
	URL[nIndex] = tempChar;
	URL += nIndex;

	// Read the port.
	/////////////////
	if(*URL == ':')
	{
		URL++;
		connection->serverPort = (unsigned short)atoi(URL);
		if(!connection->serverPort)
			return GHTTPFalse;
		do
		{
			URL++;
		}while(*URL && (*URL != '/'));
	}
	else
	{
		if (connection->protocol == GHIHttps)
			connection->serverPort = GHI_DEFAULT_SECURE_PORT;
		else
			connection->serverPort = GHI_DEFAULT_PORT;
	}

	// Read the path.
	/////////////////
	if(!*URL)
		URL = "/";
	connection->requestPath = goastrdup(URL);
	while((str = strchr(connection->requestPath, ' ')) != NULL)
		*str = '+';
	if(!connection->requestPath)
		return GHTTPFalse;

	return GHTTPTrue;
}

/****************
** SOCKET INIT **
****************/
void ghiDoSocketInit
(
	GHIConnection * connection
)
{
	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Socket Initialization\n");

	// Progress.
	////////////
	ghiCallProgressCallback(connection, NULL, 0);

	// Init sockets.
	////////////////
	SocketStartUp();

	// Parse the URL.
	/////////////////
	if(!ghiParseURL(connection))
	{
		connection->completed = GHTTPTrue;
		connection->result = GHTTPParseURLFailed;
		return;
	}

	// Check if an encryption type was set.
	///////////////////////////////////////
	if((connection->protocol == GHIHttps) && (connection->encryptor.mEngine == GHTTPEncryptionEngine_None))
	{
		// default to gamespy engine
		//ghttpSetRequestEncryptionEngine(connection->request, GHTTPEncryptionEngine_GameSpy);
		
		// 02OCT07 BED: Design changed so that only one engine can be active at a time
		//              Use the active engine rather than GameSpy
		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_WarmError,
			"Encryption engine not set for HTTPS.  Using default engine\r\n");
		ghttpSetRequestEncryptionEngine(connection->request, GHTTPEncryptionEngine_Default);
	}
	else if ((connection->protocol != GHIHttps) && (connection->encryptor.mEngine != GHTTPEncryptionEngine_None))
	{
		// URL is not secured
		ghttpSetRequestEncryptionEngine(connection->request, GHTTPEncryptionEngine_None);

		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_WarmError,
			"Encryption engine set for unsecured URL. Removing encryption.\r\n");
	}
	
	// Init the encryption engine.
	//////////////////////////////
	if ((connection->protocol == GHIHttps) && connection->encryptor.mInitialized == GHTTPFalse)
	{
		GHIEncryptionResult aResult;
			
		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Debug, "Initializing SSL engine\n");
		aResult = (connection->encryptor.mInitFunc)(connection, &connection->encryptor);
		if (aResult == GHIEncryptionResult_Error)
		{
			gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_WarmError, "Failed to initialize SSL engine\n");
			connection->completed = GHTTPTrue;
			connection->result = GHTTPEncryptionError;
			return;
		}
	}

	// Progress.
	////////////
	connection->state = GHTTPHostLookup;
	ghiCallProgressCallback(connection, NULL, 0);
}

/****************
** HOST LOOKUP **
****************/
void ghiDoHostLookup
(
	GHIConnection * connection
)
{
	HOSTENT * host = NULL;
	const char * server = NULL;

#if !defined(GSI_NO_THREADS)
	// Check to see if asynch lookup is taking place
    ////////////////////////////////////////////////
	if (connection->handle)
	{
		GSI_UNUSED(host);
		GSI_UNUSED(server);
		
		// Lookup incomplete - set to lookupPending state
        /////////////////////////////////////////////////
		connection->state = GHTTPLookupPending;
		ghiCallProgressCallback(connection, NULL, 0);
		return;
	}
#endif

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Host Lookup\n");


	// Check for using a proxy.
	///////////////////////////
	if (connection->proxyOverrideServer) // request specific proxy
		server = connection->proxyOverrideServer;
	else if(ghiProxyAddress)
		server = ghiProxyAddress;
	else
		server = connection->serverAddress;

	// Try resolving the address as an IP a.b.c.d number.
	/////////////////////////////////////////////////////
	connection->serverIP = inet_addr(server);
	if(connection->serverIP == INADDR_NONE)
	{
		// Try resolving with DNS - asynchronously if possible
		//////////////////////////

#if defined(GSI_NO_THREADS)
		//blocking version - no threads
		host = gethostbyname(server);

		if(host == NULL)
		{
			gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_HotError, 
				"Host Lookup failed\n");
			connection->completed = GHTTPTrue;
			connection->result = GHTTPHostLookupFailed;
			return;
		}

		// Get the IP.
		//////////////
		connection->serverIP = *(unsigned int *)host->h_addr_list[0];
#else
		
		//threaded version
		if (gsiStartResolvingHostname(server, &(connection->handle)) == -1)
		{
			gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_HotError, 
				"Thread Creation Failed\n");

			//make sure to set it back to NULL
			connection->handle = NULL;

			//exit with Host Lookup Failed error message
			connection->completed = GHTTPTrue;
			connection->result = GHTTPHostLookupFailed;
			return;
		}
		else
		{
			//thread created properly - continue into lookupPending state
			GSI_UNUSED(host);
		}
#endif
	}

	// Progress.
	////////////

	//check to see if lookup is complete
	if (connection->serverIP == INADDR_NONE)
	{
		//lookup incomplete - set to lookupPending state
		connection->state = GHTTPLookupPending;
		ghiCallProgressCallback(connection, NULL, 0);
	}
	else
	{
		//lookup complete - proceed with connection stage
		connection->state = GHTTPConnecting;
		ghiCallProgressCallback(connection, NULL, 0);
	}
}

/******************
** LOOKUP PENDING**
******************/
void ghiDoLookupPending
(
	GHIConnection * connection
)
{
#if !defined(GSI_NO_THREADS)
	//check if lookup is complete
	connection->serverIP = gsiGetResolvedIP(connection->handle);

	//make sure there were no problems with the IP
	if (connection->serverIP == GSI_ERROR_RESOLVING_HOSTNAME)
	{
		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_HotError, 
			"Error resolving hostname\n");

		//set to NULL
		connection->handle = NULL;

		//notify that the lookup failed
		connection->completed = GHTTPTrue;
		connection->result = GHTTPHostLookupFailed;
		return;
	}

	if (connection->serverIP == GSI_STILL_RESOLVING_HOSTNAME)
	{
		//lookup incomplete - keep calling this function
		connection->state = GHTTPLookupPending;
		ghiCallProgressCallback(connection, NULL, 0);
	}
	else
	{
		//set to NULL
		connection->handle = NULL;
		
		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, 
			"DNS lookup complete\n");
		//looks like we got ourselves a server! proceed with connection phase
		connection->state = GHTTPConnecting;
		ghiCallProgressCallback(connection, NULL, 0);
	}
#endif
}

/***************
** CONNECTING **
***************/
void ghiDoConnecting
(
	GHIConnection * connection
)
{
	int rcode;
	SOCKADDR_IN address;
	int writeFlag;
	int exceptFlag;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Connecting\n");

	// If we don't have a socket yet, set it up.
	////////////////////////////////////////////
	if(connection->socket == INVALID_SOCKET)
	{
		// Create the socket.
		/////////////////////
		connection->socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(connection->socket == INVALID_SOCKET)
		{
			connection->completed = GHTTPTrue;
			connection->result = GHTTPSocketFailed;
			connection->socketError = GOAGetLastError(connection->socket);
			return;
		}

		// Set the socket to non-blocking.
		//////////////////////////////////
		if(!SetSockBlocking(connection->socket, 0))
		{
			connection->completed = GHTTPTrue;
			connection->result = GHTTPSocketFailed;
			connection->socketError = GOAGetLastError(connection->socket);
			return;
		}

		// If throttling, use a small receive buffer.
		/////////////////////////////////////////////
		if(connection->throttle)
			SetReceiveBufferSize(connection->socket, ghiThrottleBufferSize);

		// Setup the server address.
		////////////////////////////
		memset(&address, 0, sizeof(SOCKADDR_IN));
		address.sin_family = AF_INET;
		if (connection->proxyOverrideServer)
			address.sin_port = htons(connection->proxyOverridePort);
		else if(ghiProxyAddress)
			address.sin_port = htons(ghiProxyPort);
		else
			address.sin_port = htons(connection->serverPort);
		address.sin_addr.s_addr = connection->serverIP;

		// Start the connect.
		/////////////////////
		rcode = connect(connection->socket, (SOCKADDR *)&address, sizeof(address));
		if(gsiSocketIsError(rcode))
		{
			int socketError = GOAGetLastError(connection->socket);
			if((socketError != WSAEWOULDBLOCK) && (socketError != WSAEINPROGRESS) && (socketError != WSAETIMEDOUT))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPConnectFailed;
				connection->socketError = socketError;
				return;
			}
		}
	}

	// Check if the connect has completed.
	//////////////////////////////////////
	rcode = GSISocketSelect(connection->socket, NULL, &writeFlag, &exceptFlag);
	if((gsiSocketIsError(rcode)) || ((rcode == 1) && exceptFlag))
	{
		connection->completed = GHTTPTrue;
		connection->result = GHTTPConnectFailed;
		if(gsiSocketIsError(rcode))
			connection->socketError = GOAGetLastError(connection->socket);
		else
			connection->socketError = 0;
		return;
	}

	// Check if we're connected.
	////////////////////////////
	if((rcode == 1) && writeFlag)
	{
		// Progress.
		////////////
		if (connection->encryptor.mEngine == GHTTPEncryptionEngine_None)
			connection->state = GHTTPSendingRequest;
		else
			connection->state = GHTTPSecuringSession;
		ghiCallProgressCallback(connection, NULL, 0);
	}
} 

/******************
** SSL HANDSHAKE **
*******************/
void ghiDoSecuringSession
(
	GHIConnection * connection
)
{
	// Client sends hello
	// Server sends hello, [certificate], [certificate request], [server key exchange]
	// Client sends client <key exchange>, <finished>, [certificate], [certificate verify]
	// Server sends finished

	// skip the ghiDoSecuringSession step...
	//     - when not using encryption or 
	//     - if the connection is already secure

	GHIRecvResult result;
	
	// This buffer must be large enough to receive any handshake messages.
	char buffer[1025];
	int bufferLen;

	// Start the handshake process
	if (connection->encryptor.mSessionStarted == GHTTPFalse)
	{
		GHIEncryptionResult aResult;
		
		gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Securing Session\n");

		GS_ASSERT(connection->encryptor.mStartFunc != NULL);
		if (connection->encryptor.mStartFunc != NULL)
		{
			aResult = (connection->encryptor.mStartFunc)(connection, &connection->encryptor);
			if (aResult == GHIEncryptionResult_Error)
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}
		}
		
		// Check for session established
		if (connection->encryptor.mSessionEstablished)
		{
			connection->state = GHTTPSendingRequest;
			ghiCallProgressCallback(connection, NULL, 0);
			return;
		}
	}
	
	// if the SSL lib controls the handshake, just keep calling
	// start until the session has been established
	GS_ASSERT(connection->encryptor.mSessionEstablished == GHTTPFalse);
	if (connection->encryptor.mLibSendsHandshakeMessages)
	{
		GS_ASSERT(connection->encryptor.mStartFunc != NULL);
		if (connection->encryptor.mStartFunc != NULL)
		{
			GHIEncryptionResult aResult = (connection->encryptor.mStartFunc)(connection, &connection->encryptor);
			if (aResult == GHIEncryptionResult_Error)
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}
		}
		
		// Check for session established
		if (connection->encryptor.mSessionEstablished)
		{
			connection->state = GHTTPSendingRequest;
			ghiCallProgressCallback(connection, NULL, 0);
		}
	}
	else
	{
		// Continue to send and receive handshake messages until the session has been secured
		// Send any session messages
		if (connection->sendBuffer.pos < connection->sendBuffer.len)
		{
			if (!ghiSendBufferedData(connection))
				return; // Todo: handle error?

			// Check for data still buffered.
			/////////////////////////////////
			if(connection->sendBuffer.pos < connection->sendBuffer.len)
				return;

			ghiResetBuffer(&connection->sendBuffer);
		}

		// Get data
		bufferLen = sizeof(buffer);
		result = ghiDoReceive(connection, buffer, &bufferLen);
		
		// Handle error or conn closed.
		///////////////////////////////
		if((result == GHIError) || (result == GHIConnClosed))
		{
			connection->completed = GHTTPTrue;
			connection->result = GHTTPEncryptionError;
			return;
		}

		// check for received data
		if(result == GHIRecvData)
		{
			// Append new encrypted data to anything we've held over
			//    We have to do this because we can't decrypt partial SSL messages
			if (!ghiAppendDataToBuffer(&connection->decodeBuffer, buffer, bufferLen))
				return;

			// Decrypt as much as we can
			if (!ghiDecryptReceivedData(connection))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}

			// Check for session established (handshake complete)
			if (connection->encryptor.mSessionEstablished)
			{
				connection->state = GHTTPSendingRequest;
				ghiCallProgressCallback(connection, NULL, 0);
				return;
			}
		}
	}
}


/********************
** SENDING REQUEST **
********************/
void ghiDoSendingRequest
(
	GHIConnection * connection
)
{
	char * requestType;
	int oldPos;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Sending Request\n");
	
	// If we haven't filled the send buffer yet, do that first.
	///////////////////////////////////////////////////////////
	if(!connection->sendBuffer.len)
	{
		// Using a pointer so we can pipe output to a different destination
		//    (e.g. for efficiency and testing purposes we may want to encrypt in larger blocks)
		GHIBuffer* writeBuffer = NULL;
		if (connection->encryptor.mEngine == GHTTPEncryptionEngine_None ||
			connection->encryptor.mEncryptOnBuffer == GHTTPFalse)
		{
		    // write directly to send buffer
			writeBuffer = &connection->sendBuffer;
		}
		else
		{
			// write to temp buffer so it can be encrypted before sending
			writeBuffer = &connection->encodeBuffer;
		}

		// Fill in the request line.
		////////////////////////////
		if(connection->post && !connection->postingState.completed)
			requestType = "POST ";
		else if(connection->type == GHIHEAD)
			requestType = "HEAD ";
		else
			requestType = "GET ";
		ghiAppendDataToBuffer(writeBuffer, requestType, 0);
		if (connection->proxyOverrideServer || ghiProxyAddress)
			ghiAppendDataToBuffer(writeBuffer, connection->URL, 0);
		else
			ghiAppendDataToBuffer(writeBuffer, connection->requestPath, 0);
		ghiAppendDataToBuffer(writeBuffer, " HTTP/1.1" CRLF, 0);

		// Add the host header.
 		///////////////////////
		if(connection->serverPort == GHI_DEFAULT_PORT)
		{
			ghiAppendHeaderToBuffer(writeBuffer, "Host", connection->serverAddress);
		}
		else
		{
			ghiAppendDataToBuffer(writeBuffer, "Host: ", 0);
			ghiAppendDataToBuffer(writeBuffer, connection->serverAddress, 0);
			ghiAppendCharToBuffer(writeBuffer, ':');
			ghiAppendIntToBuffer(writeBuffer, connection->serverPort);
			ghiAppendDataToBuffer(writeBuffer, CRLF, 2);
		}

		// Add the user-agent header.
		/////////////////////////////
		if (connection->sendHeaders == NULL || strstr(connection->sendHeaders, "User-Agent")==NULL)
			ghiAppendHeaderToBuffer(writeBuffer, "User-Agent", "GameSpyHTTP/1.0");
		
		// Check for persistant connections.
		//////////////////////////////////////
		if (connection->persistConnection)
			ghiAppendHeaderToBuffer(writeBuffer, "Connection", "Keep-Alive");
		else
			ghiAppendHeaderToBuffer(writeBuffer, "Connection", "close");

		// Post needs extra headers.
		////////////////////////////
		if(connection->post && !connection->postingState.completed)
		{
			char buf[16];

			// Add the content-length header.
			/////////////////////////////////
			sprintf(buf, "%d", connection->postingState.totalBytes);
			ghiAppendHeaderToBuffer(writeBuffer, "Content-Length", buf);

			// Add the content-type header.
			///////////////////////////////
			ghiAppendHeaderToBuffer(writeBuffer, "Content-Type", ghiPostGetContentType(connection));
		}

		// Not supported by all servers
		//ghiAppendHeaderToBuffer(writeBuffer, "Expect", "100-continue");

		// Add user-headers.
		////////////////////
		if(connection->sendHeaders)
			ghiAppendDataToBuffer(writeBuffer, connection->sendHeaders, 0);

		// Add the blank line to finish it off.
		///////////////////////////////////////
		ghiAppendDataToBuffer(writeBuffer, CRLF, 2);

		// Encrypt it, if necessary.  This copy is unfortunate since matrixSsl can't encrypt in place
		if (connection->encryptor.mEngine != GHTTPEncryptionEngine_None &&
			connection->encryptor.mEncryptOnBuffer == GHTTPTrue)
		{
			GS_ASSERT(writeBuffer == &connection->encodeBuffer);
			if (!ghiEncryptDataToBuffer(&connection->sendBuffer, writeBuffer->data, writeBuffer->len))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}
			ghiResetBuffer(writeBuffer);
		}
	}

	// Store the old position.
	//////////////////////////
	oldPos = connection->sendBuffer.pos;

	// Send what we can.
	////////////////////
	if(!ghiSendBufferedData(connection))
		return;
	
	// Log anything we sent.
	////////////////////////
	#ifdef HTTP_LOG
	if(connection->sendBuffer.pos != oldPos)
		ghiLogRequest(connection->sendBuffer.data + oldPos, connection->sendBuffer.pos - oldPos);
	#endif

	// Check for data still buffered.
	/////////////////////////////////
	if(connection->sendBuffer.pos < connection->sendBuffer.len)
		return;

	// Clear the send buffer.
	/////////////////////////
	ghiResetBuffer(&connection->sendBuffer);

	// Finished sending.
	////////////////////
	if(connection->post && !connection->postingState.completed)
		connection->state = GHTTPPosting;
	else
		connection->state = GHTTPWaiting;
	ghiCallProgressCallback(connection, NULL, 0);

	GSI_UNUSED(oldPos);
}

/************
** POSTING **
************/
void ghiDoPosting
(
	GHIConnection * connection
)
{
	GHIPostingResult result;
	int oldBytesPosted;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Posting\n");

	// Store the old bytes posted.
	//////////////////////////////
	oldBytesPosted = connection->postingState.bytesPosted;

	// Do some posting.
	///////////////////
	result = ghiPostDoPosting(connection);

	// Check for an error.
	//////////////////////
	if(result == GHIPostingError)
	{
		int rcode = 0;
		int readFlag = 0;

		// Make sure we already set the error stuff.
		////////////////////////////////////////////
		assert(connection->completed && connection->result);

		// Cleanup the posting state.
		/////////////////////////////
		ghiPostCleanupState(connection);

		// Is there a server response?
		rcode = GSISocketSelect(connection->socket, &readFlag, NULL, NULL);
		if((rcode == 1) && readFlag)
		{
			// Ready to receive.
			////////////////////
			connection->state = GHTTPReceivingStatus;
			ghiCallProgressCallback(connection, NULL, 0);
		}
		return;
	}

	// When sending DIME wait for initial 
	// continue before uploading
	/////////////////////////////////////////
	if (result == GHIPostingWaitForContinue)
	{
		// Disable by skipping the wait
		connection->postingState.waitPostContinue = GHTTPFalse;
		return;

		//connection->state = GHTTPWaiting;
		//return;
	}

	// Call the callback if we sent anything.
	/////////////////////////////////////////
	if(oldBytesPosted != connection->postingState.bytesPosted)
		ghiCallPostCallback(connection);

	// Check for done.
	//////////////////
	if(result == GHIPostingDone)
	{
		// Cleanup the posting state.
		/////////////////////////////
		ghiPostCleanupState(connection);
		connection->postingState.completed = GHTTPTrue;

		// Set the new connection state.
		////////////////////////////////
		connection->state = GHTTPWaiting;
		ghiCallProgressCallback(connection, NULL, 0);

		return;
	}
}

/************
** WAITING **
************/
void ghiDoWaiting
(
	GHIConnection * connection
)
{
	int readFlag;
	int exceptFlag;
	int rcode;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Waiting\n");

	// We're waiting to receive something.
	//////////////////////////////////////
	rcode = GSISocketSelect(connection->socket, &readFlag, NULL, &exceptFlag);
	if((gsiSocketIsError(rcode)) || ((rcode == 1) && exceptFlag))
	{
		connection->completed = GHTTPTrue;
		connection->result = GHTTPSocketFailed;
		if(gsiSocketIsError(rcode))
			connection->socketError = GOAGetLastError(connection->socket);
		else
			connection->socketError = 0;
		return;
	}

	// Check for waiting data.
	//////////////////////////
	if((rcode == 1) && readFlag)
	{
		// Ready to receive.
		////////////////////
		connection->state = GHTTPReceivingStatus;
		ghiCallProgressCallback(connection, NULL, 0);
	}
}

// Parse the status line.
/////////////////////////
static GHTTPBool ghiParseStatus
(
	GHIConnection * connection
)
{
	int majorVersion;
	int minorVersion;
	int statusCode;
	int statusStringIndex;
	int rcode;
	char c;

	GS_ASSERT(connection);
	GS_ASSERT(connection->recvBuffer.len > 0);

#if defined(_X360)
	//Xbox 360 needs "%n" to be manually enabled
	{
	int oldPrintCountValue = _set_printf_count_output(1);
#endif

	// Parse the string.
	////////////////////
	rcode = sscanf(connection->recvBuffer.data, "HTTP/%d.%d %d%n",
		&majorVersion,
		&minorVersion,
		&statusCode,
		&statusStringIndex);

#if defined(_X360)
	_set_printf_count_output(oldPrintCountValue);
	}
#endif

	// Check what we got.
	/////////////////////
	if((rcode != 3) ||     // Not all fields read.
		//!*statusString ||  // No status string.  PANTS|9.16.02 - apparently some servers don't return a status string
		(majorVersion < 1) ||  // Major version is less than 1.
		(statusCode < 100) ||        // 1xx is lowest status code.
		(statusCode >= 600))         // 5xx is highest status code.
	{
		connection->completed = GHTTPTrue;
		connection->result = GHTTPBadResponse;
		return GHTTPFalse;
	}

	// Figure out where the status string starts.
	/////////////////////////////////////////////
	while((c = connection->recvBuffer.data[statusStringIndex]) != '\0' && isspace(c))
		statusStringIndex++;

	// Set connection members.
	//////////////////////////
	connection->statusMajorVersion = majorVersion;
	connection->statusMinorVersion = minorVersion;
	connection->statusCode = statusCode;
	connection->statusStringIndex = statusStringIndex;

	return GHTTPTrue;
}

/*********************
** RECEIVING STATUS **
*********************/
void ghiDoReceivingStatus
(
	GHIConnection * connection
)
{
	char buffer[1024];
	int bufferLen;
	GHIRecvResult result;
	char * endOfStatus;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Receiving Status\n");

	// Get data.
	////////////
	bufferLen = sizeof(buffer);
	result = ghiDoReceive(connection, buffer, &bufferLen);

	// Handle error or no data.
	///////////////////////////
	if(result == GHIError)
		return;
	if(result == GHINoData)
		return;

	// Only append data if we got data.
	///////////////////////////////////
	if(result == GHIRecvData)
	{
		// Check for encryption.
		////////////////////////
		if (connection->encryptor.mEngine != GHTTPEncryptionEngine_None &&
			connection->encryptor.mEncryptOnBuffer == GHTTPTrue)
		{
			// Append new encrypted data to anything we've held over
			//    We have to do this because we can't decrypt partial SSL messages
			if (!ghiAppendDataToBuffer(&connection->decodeBuffer, buffer, bufferLen))
				return;

			// Decrypt as much as we can
			if (!ghiDecryptReceivedData(connection))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}
		}
		else
		{			
			// Add the data directly to the buffer.
			///////////////////////////////////////
			if(!ghiAppendDataToBuffer(&connection->recvBuffer, buffer, bufferLen))
				return;
		}
	}

	// Check if the status is finished.
	/////////////////////////////////////
	endOfStatus = strstr(connection->recvBuffer.data, CRLF);
	if(endOfStatus)
	{
		int statusLength;

		// Cap the status.
		//////////////////
		*endOfStatus = '\0';

		// Get the status length.
		/////////////////////////
		statusLength = (endOfStatus - connection->recvBuffer.data);

		// Log it.
		//////////
		ghiLogResponse(connection->recvBuffer.data, statusLength);
		ghiLogResponse("\n", 1);

		// Parse the status line.
		/////////////////////////
		if(!ghiParseStatus(connection))
			return;

		// Store the position of the start of the headers.
		//////////////////////////////////////////////////
		connection->headerStringIndex = (statusLength + 2);


		if (connection->statusCode == 100 && connection->postingState.waitPostContinue)
		{
			// DIME uploads must wait for initial continue before posting
			connection->postingState.waitPostContinue = GHTTPFalse;
			ghiResetBuffer(&connection->recvBuffer); // clear the continue
			connection->state = GHTTPPosting;
			ghiCallProgressCallback(connection, NULL, 0);

			gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_Comment,
				"Got HTTP continue\r\n");
		}
		else
		{
			// We're receiving headers now.
			///////////////////////////////
			connection->state = GHTTPReceivingHeaders;
			ghiCallProgressCallback(connection, NULL, 0);
		}
	}
	else if(result == GHIConnClosed)
	{
		// Connection closed.
		/////////////////////
		connection->completed = GHTTPTrue;
		connection->result = GHTTPBadResponse;
		connection->socketError = GOAGetLastError(connection->socket);
		return;
	}
}

// Delivers incoming file data to the appropriate place,
// then calls the progress callback.
// For GetFile, adds to buffer.
// For SaveFile, writes to disk.
// For StreamFile, does nothing.
// Returns false on error.
////////////////////////////////////////////////////////
static GHTTPBool ghiDeliverIncomingFileData
(
	GHIConnection * connection,
	char * data,
	int len
)
{
	char * buffer = NULL;
	int bufferLen = 0;

	// Add this to the total.
	/////////////////////////
	connection->fileBytesReceived += len;

	// Do we have the whole thing?
	//////////////////////////////
	if(connection->fileBytesReceived == connection->totalSize || connection->connectionClosed)
		connection->completed = GHTTPTrue;

	// Handle based on type.
	////////////////////////
	if(connection->type == GHIGET)
	{
		// Put this in the buffer.
		//////////////////////////
		if(!ghiAppendDataToBuffer(&connection->getFileBuffer, data, len))
			return GHTTPFalse;

		// Set the callback parameters.
		///////////////////////////////
		buffer = connection->getFileBuffer.data;
		bufferLen = connection->getFileBuffer.len;
	}
	else if(connection->type == GHISAVE)
	{
		int bytesWritten = 0;
#ifndef NOFILE
		bytesWritten = fwrite(data, 1, len, connection->saveFile);
#endif
		if(bytesWritten != len)
		{
			connection->completed = GHTTPTrue;
			connection->result = GHTTPFileWriteFailed;
			return GHTTPFalse;
		}

		// Set the callback parameters.
		///////////////////////////////
		buffer = data;
		bufferLen = len;
	}
	else if(connection->type == GHISTREAM)
	{
		// Set the callback parameters.
		///////////////////////////////
		buffer = data;
		bufferLen = len;
	}

	// Call the callback.
	/////////////////////
	ghiCallProgressCallback(connection, buffer, bufferLen);

	return GHTTPTrue;
}

// Gets the size of a chunk from a chunk header.
// Returns -1 on error.
////////////////////////////////////////////////
static int ghiParseChunkSize
(
	GHIConnection * connection
)
{
	char * header;
	int len;
	int num;
	int rcode;

	header = connection->chunkHeader;
	len = connection->chunkHeaderLen;

	assert(len);
	GSI_UNUSED(len);

	rcode = sscanf(header, "%x", &num);
	if(rcode != 1)
		return -1;

	return num;
}

// Appends the data to the chunk header buffer.
///////////////////////////////////////////////
static void ghiAppendToChunkHeaderBuffer
(
	GHIConnection * connection,
	char * data,
	int len
)
{
	assert(connection);
	assert(data);
	assert(len >= 0);

	// This can happen at the end of a header.
	//////////////////////////////////////////
	if(len == 0)
		return;

	// Is there room in the buffer?  If not, just
	// skip, we most likely already have the chunk size.
	////////////////////////////////////////////////////
	if(connection->chunkHeaderLen < CHUNK_HEADER_SIZE)
	{
		int numBytes;

		// How many bytes are we copying?
		/////////////////////////////////
		numBytes = min(CHUNK_HEADER_SIZE - connection->chunkHeaderLen, len);
		
		// Move the (possibly partial) header into the buffer.
		//////////////////////////////////////////////////////
		memcpy(connection->chunkHeader + connection->chunkHeaderLen, data, (unsigned int)numBytes);

		// Cap off the buffer.
		//////////////////////
		connection->chunkHeaderLen += numBytes;
		connection->chunkHeader[connection->chunkHeaderLen] = '\0';
	}
}

// Does any neccessary processing to incoming file data
// before it gets delivered.  This includes un-chunking.
// Returns false on error.
////////////////////////////////////////////////////////
static GHTTPBool ghiProcessIncomingFileData
(
	GHIConnection * connection,
	char * data,
	int len
)
{
	assert(connection);
	assert(data);
	assert(len > 0);

	// Is this a chunked transfer?
	//////////////////////////////
	if(connection->chunkedTransfer)
	{
		// Loop while there's stuff to process.
		///////////////////////////////////////
		while(len > 0)
		{
			// Reading a header?
			////////////////////
			if(connection->chunkReadingState == CRHeader)
			{
				char * endOfHeader;

				// Have we hit the LF (as in the CRLF ending the header)?
				/////////////////////////////////////////////////////////
				endOfHeader = strchr(data, 0xA);
				if(endOfHeader)
				{
					// Append what we have to the buffer.
					/////////////////////////////////////
					ghiAppendToChunkHeaderBuffer(connection, data, endOfHeader - data);

					// Adjust data and len.
					///////////////////////
					endOfHeader++;
					len -= (endOfHeader - data);
					data = endOfHeader;

					// Read the chunk size.
					///////////////////////
					connection->chunkBytesLeft = ghiParseChunkSize(connection);
					if(connection->chunkBytesLeft == -1)
					{
						// There was an error reading the chunk size.
						/////////////////////////////////////////////
						connection->completed = GHTTPTrue;
						connection->result = GHTTPBadResponse;
						return GHTTPFalse;
					}

					// Set the chunk reading state.
					///////////////////////////////
					if(connection->chunkBytesLeft == 0)
					{
						connection->chunkReadingState = CRFooter;
						gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_RawDump,
							"Reading footer\n");
					}
					else
					{
						connection->chunkReadingState = CRChunk;
						gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_RawDump,
							"Reading %d byte chunk\n", connection->chunkBytesLeft);
					}
				}
				else
				{
					// Move it all into the buffer.
					///////////////////////////////
					ghiAppendToChunkHeaderBuffer(connection, data, len);

					// Nothing else we can do now.
					//////////////////////////////
					return GHTTPTrue;
				}
			}
			// Reading a chunk?
			///////////////////
			else if(connection->chunkReadingState == CRChunk)
			{
				int numBytes;

				// How many bytes of data are we dealing with?
				//////////////////////////////////////////////
				numBytes = min(connection->chunkBytesLeft, len);

				gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_RawDump,
					"Read %d bytes of chunk\n", numBytes);

				// Deliver the bytes.
				/////////////////////
				if(!ghiDeliverIncomingFileData(connection, data, numBytes))
					return GHTTPFalse;

				// Adjust data and len.
				///////////////////////
				data += numBytes;
				len -= numBytes;

				// Figure out how many bytes left in chunk.
				///////////////////////////////////////////
				connection->chunkBytesLeft -= numBytes;

				// Did we finish the chunk?
				///////////////////////////
				if(connection->chunkBytesLeft == 0)
					connection->chunkReadingState = CRCRLF;
			}
			// Reading a chunk footer (CRLF)?
			/////////////////////////////////
			else if(connection->chunkReadingState == CRCRLF)
			{
				char * endOfFooter;

				// Did we get an LF?
				////////////////////
				endOfFooter = strchr(data, 0xA);

				// The footer hasn't ended yet.
				///////////////////////////////
				if(!endOfFooter)
					return GHTTPTrue;

				// Adjust data and len.
				///////////////////////
				endOfFooter++;
				len -= (endOfFooter - data);
				data = endOfFooter;

				// Set up for reading the next header.
				//////////////////////////////////////
				connection->chunkHeader[0] = '\0';
				connection->chunkHeaderLen = 0;
				connection->chunkBytesLeft = 0;
				connection->chunkReadingState = CRHeader;

				gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_RawDump,
					"Read chunk footer\n");
			}
			// Reading the footer?
			//////////////////////
			else if(connection->chunkReadingState == CRFooter)
			{
				// We're done.
				//////////////
				connection->completed = GHTTPTrue;

				gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_Network, GSIDebugLevel_RawDump,
					"Finished reading chunks\n");

				return GHTTPTrue;
			}
			// Bad state!
			/////////////
			else
			{
				assert(0);
				return GHTTPFalse;
			}
		}

		return GHTTPTrue;
	}

	// Regular transfer, just deliver it.
	/////////////////////////////////////
	return ghiDeliverIncomingFileData(connection, data, len);
}

/**********************
** RECEIVING HEADERS **
**********************/
void ghiDoReceivingHeaders
(
	GHIConnection * connection
)
{
	char buffer[4096];
	int bufferLen;
	GHIRecvResult result;
	GHTTPBool hasHeaders = GHTTPTrue;
	char * headers;
	char * endOfHeaders = NULL;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Receiving Headers\n");

	// Get data.
	////////////
	bufferLen = sizeof(buffer);
	result = ghiDoReceive(connection, buffer, &bufferLen);

	// Handle error, no data, conn closed.
	//////////////////////////////////////
	if(result == GHIError)
		return;
	if(result == GHINoData)
		return;

	// Only append data if we got data.
	///////////////////////////////////
	if(result == GHIRecvData)
	{
		// Check for encryption.
		////////////////////////
		if (connection->encryptor.mEngine != GHTTPEncryptionEngine_None &&
			connection->encryptor.mEncryptOnBuffer == GHTTPTrue)
		{
			// Append new encrypted data to anything we've held over
			//    We have to do this because we can't decrypt partial SSL messages
			if (!ghiAppendDataToBuffer(&connection->decodeBuffer, buffer, bufferLen))
				return;

			// Decrypt as much as we can
			if (!ghiDecryptReceivedData(connection))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}
		}
		else
		{
			// Add the data directly to the buffer.
			///////////////////////////////////////
			if(!ghiAppendDataToBuffer(&connection->recvBuffer, buffer, bufferLen))
				return;
		}
	}

	// Cache a pointer to the front of the headers.
	///////////////////////////////////////////////
	headers = (connection->recvBuffer.data + connection->headerStringIndex);

	// Check if the headers are finished.
	/////////////////////////////////////
	if( ((connection->statusCode / 100) == 1) &&
		 (strncmp(headers, "\r\n", 2) == 0 || strncmp(headers, "\xA\xA", 2) == 0)
		 )
	{
		// If a continue doesn't have a header (immediate CRLF) move on to next status
		endOfHeaders = headers;
		hasHeaders = GHTTPFalse;
	}
	else
	{
		endOfHeaders = strstr(headers, CRLF CRLF);
	}
	if(!endOfHeaders)
	{
		endOfHeaders = strstr(headers, "\xA\xA"); // some servers seem to use LFs only?! Seen in 302 redirect. (28may01/bgw)
	}
	if(endOfHeaders)
	{
		char * fileStart;
		int fileLength;
		//int headersLength;
		char * contentLength;

#ifdef HTTP_LOG
		int headersLength;
#endif

		// Clear off the empty line.
		////////////////////////////
		if (GHTTPTrue == hasHeaders)
			endOfHeaders += 2;
		*endOfHeaders = '\0';

		// Figure out where the file starts, and how many bytes.
		////////////////////////////////////////////////////////
#ifdef HTTP_LOG
		headersLength = (endOfHeaders - headers);
#endif

		fileStart = (endOfHeaders + 2);
		fileLength = (connection->recvBuffer.len - (fileStart - connection->recvBuffer.data));

		// Set the headers buffer's new length.
		///////////////////////////////////////
		connection->recvBuffer.len = (endOfHeaders - connection->recvBuffer.data + 1);
		connection->recvBuffer.pos = connection->recvBuffer.len;

		// Log it.
		//////////
#ifdef HTTP_LOG
		ghiLogResponse(headers, headersLength);
		ghiLogResponse("\n", 1);
#endif

		// Check for continue.
		//////////////////////
		if((connection->statusCode / 100) == 1)
		{
			if(fileLength)
			{
				// Move any data to the front of the buffer.
				////////////////////////////////////////////
				memmove(connection->recvBuffer.data, fileStart, (unsigned int)fileLength + 1);
				connection->recvBuffer.len = fileLength;
			}
			else
			{
				// Reset the buffer.
				/////////////////////////
				ghiResetBuffer(&connection->recvBuffer);
			}

			// Some posts must wait for continue before uploading
			// Check if we should return to posting
			if (connection->postingState.waitPostContinue)
			{
				connection->postingState.waitPostContinue = GHTTPFalse;
				connection->state = GHTTPPosting;
				ghiCallProgressCallback(connection, NULL, 0);
			}

			// We're back to receiving status.
			//////////////////////////////////
			connection->state = GHTTPReceivingStatus;
			ghiCallProgressCallback(connection, NULL, 0);

			return;
		}

		// Check for redirection.
		/////////////////////////
		if((connection->statusCode / 100) == 3)
		{
			char * location;

			// Are we over our redirection count?
			/////////////////////////////////////
			if(connection->redirectCount > 10)
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPFileNotFound;
				return;
			}

			// Find the new location.
			/////////////////////////
			location = strstr(headers, "Location:");
			if(location)
			{
				char * end;

				// Find the start of the URL.
				/////////////////////////////
				location += 9;
				while(isspace(*location))
					location++;

				// Find the end.
				////////////////
				for(end = location; *end && !isspace(*end) ; end++)  { };
				*end = '\0';

				// Check if this is not a full URL.
				///////////////////////////////////
				if(*location == '/')
				{
					int len;

					// Recompose the URL ourselves.
					///////////////////////////////
					len = (int)(strlen(connection->serverAddress) + 13 + strlen(location) + 1);
					connection->redirectURL = (char *)gsimalloc((unsigned int)len);
					if(!connection->redirectURL)
					{
						connection->completed = GHTTPTrue;
						connection->result = GHTTPOutOfMemory;
					}
					sprintf(connection->redirectURL, "http://%s:%d%s", connection->serverAddress, connection->serverPort, location);
				}
				else
				{
					// Set the redirect URL.
					////////////////////////
					connection->redirectURL = goastrdup(location);
					if(!connection->redirectURL)
					{
						connection->completed = GHTTPTrue;
						connection->result = GHTTPOutOfMemory;
					}
				}

				return;
			}
		}

		// If we know the file-length, set it.
		//////////////////////////////////////
		contentLength = strstr(headers, "Content-Length:");
		if(contentLength)
		{
			// Verify that the download size is something we can handle
			///////////////////////////////////////////////////////////
#if (GSI_MAX_INTEGRAL_BITS >= 64)
			char  szMaxSize[] = "9223372036854775807"; // == GSI_MAX_I64       
#else
			char  szMaxSize[] = "2147483647";          // == GSI_MAX_I32
#endif
			char* pStart  = contentLength+16;
			char* pEnd    = pStart;
			int   nMaxLen = (int)strlen(szMaxSize);

			// Skip to the end of the line
			while( pEnd && *pEnd != '\0' && *pEnd != '\n' && *pEnd != '\r' && *pEnd != ' ' )
				pEnd++;

 			if( pEnd-pStart > nMaxLen )
			{
				// Wow, that IS a big number
				connection->completed = GHTTPTrue;
				connection->result = GHTTPFileToBig;
				return;
			}
			else
			if( pEnd-pStart == nMaxLen )
			{
				// Same length, maybe a bigger number
				if( strncmp(pStart,szMaxSize,(unsigned int)(pEnd-pStart)) >= 0 )
				{
					connection->completed = GHTTPTrue;
					connection->result = GHTTPFileToBig;
					return;
				}
			}

			// Record the full size of the expected download
			////////////////////////////////////////////////
#if (GSI_MAX_INTEGRAL_BITS >= 64)
			connection->totalSize = _atoi64(pStart);
#else
			connection->totalSize = atoi(pStart);
#endif
		}

		// Check the chunky.
		////////////////////
		connection->chunkedTransfer = (strstr(headers, "Transfer-Encoding: chunked") != NULL)?GHTTPTrue:GHTTPFalse;
		if(connection->chunkedTransfer)
		{
			connection->chunkHeader[0] = '\0';
			connection->chunkHeaderLen = 0;
			connection->chunkBytesLeft = 0;
			connection->chunkReadingState = CRHeader;
		}

		// If we're just getting headers, or only posting data, we're done.
		///////////////////////////////////////////////////////////////////
		if((connection->type == GHIHEAD) || (connection->type == GHIPOST))
		{
			connection->completed = GHTTPTrue;
			return;
		}

		// We're receiving file data now.
		/////////////////////////////////
		connection->state = GHTTPReceivingFile;

		// Is this an empty file?
		/////////////////////////
		if(contentLength && !connection->totalSize)
		{
			connection->completed = GHTTPTrue;
			return;
		}

		// If any of the body has arrived, handle it.
		/////////////////////////////////////////////
		if(fileLength > 0)
			ghiProcessIncomingFileData(connection, fileStart, fileLength);

		// Don't reset the buffer -- we store status and header info
		//ghiResetBuffer(&connection->recvBuffer);
	}
	else if(result == GHIConnClosed)
	{
		// The conn was closed, and we didn't finish the headers - bad.
		///////////////////////////////////////////////////////////////
		connection->completed = GHTTPTrue;
		connection->result = GHTTPBadResponse;
		connection->socketError = GOAGetLastError(connection->socket);
	}
}

/*******************
** RECEIVING FILE **
*******************/
void ghiDoReceivingFile
(
	GHIConnection * connection
)
{
	char buffer[8192];
	int bufferLen;
	GHIRecvResult result;
	gsi_time start_time   = current_time();
	gsi_time running_time = 0;

	gsDebugFormat(GSIDebugCat_HTTP, GSIDebugType_State, GSIDebugLevel_Comment, "Receiving File\n");

	while(!connection->completed && (running_time < connection->maxRecvTime))
	{
		// Get data.
		////////////
		bufferLen = sizeof(buffer);
		result = ghiDoReceive(connection, buffer, &bufferLen);

		// Handle error, no data, conn closed.
		//////////////////////////////////////
		if(result == GHIError)
			return;
		if(result == GHINoData)
			return;
		if(result == GHIConnClosed)
		{
			// The file is done (hopefully).
			////////////////////////////////
			connection->completed = GHTTPTrue;

			if (connection->totalSize > 0 && connection->fileBytesReceived < connection->totalSize)
				connection->result = GHTTPFileIncomplete;
			return;
		}

		// Check for encryption.
		////////////////////////
		if (connection->encryptor.mEngine != GHTTPEncryptionEngine_None &&
			connection->encryptor.mEncryptOnBuffer == GHTTPTrue)
		{
			char * decryptedData;
			int decryptedLen;

			// Append new encrypted data to anything we've held over
			//    We have to do this because we can't decrypt partial SSL messages
			if (!ghiAppendDataToBuffer(&connection->decodeBuffer, buffer, bufferLen))
				return;

			// Previously decrypted parts of the file have already been handled.
			connection->recvBuffer.len = connection->recvBuffer.pos;

			// Decrypt as much as we can
			if (!ghiDecryptReceivedData(connection))
			{
				connection->completed = GHTTPTrue;
				connection->result = GHTTPEncryptionError;
				return;
			}

			// Check for decrypted data.
			////////////////////////////
			decryptedLen = (connection->recvBuffer.len - connection->recvBuffer.pos);
			if(decryptedLen)
			{
				// Process the data.
				////////////////////
				decryptedData = (connection->recvBuffer.data + connection->recvBuffer.pos);
				if(!ghiProcessIncomingFileData(connection, decryptedData, decryptedLen))
					return;
			}
		}
		else
		{
			// Process the data.
			////////////////////
			if(!ghiProcessIncomingFileData(connection, buffer, bufferLen))
				return;
		}

		running_time = current_time() - start_time;
	}
}
