openmohaa/code/gamespy/ghttp/ghttpProcess.c

1741 lines
45 KiB
C
Raw Normal View History

2023-02-04 21:00:01 +01:00
/*
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;
}
}