You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
940 lines
28 KiB
940 lines
28 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
//-------------------------------------------------------------------------------------------------------------- |
|
// downloadthread.cpp |
|
// |
|
// Implementation file for optional HTTP asset downloading thread |
|
// Author: Matthew D. Campbell (matt@turtlerockstudios.com), 2004 |
|
//-------------------------------------------------------------------------------------------------------------- |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
// Includes |
|
//-------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
#if defined( WIN32 ) && !defined( _X360 ) |
|
#include "winlite.h" |
|
#include <WinInet.h> |
|
#endif |
|
#include <assert.h> |
|
#include <sys/stat.h> |
|
#include <stdio.h> |
|
|
|
#include "tier0/platform.h" |
|
#include "tier0/dbg.h" |
|
#include "download_internal.h" |
|
#include "tier1/strtools.h" |
|
#include "tier0/threadtools.h" |
|
|
|
#if defined( _X360 ) |
|
#include "xbox/xbox_win32stubs.h" |
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
|
|
void WriteFileFromRequestContext( const RequestContext_t &rc ) |
|
{ |
|
struct stat buf; |
|
int rt = stat(rc.absLocalPath, &buf); |
|
if ( rt == -1 && !rc.bSuppressFileWrite ) |
|
{ |
|
FILE *fp = fopen( rc.absLocalPath, "wb" ); |
|
if ( fp ) |
|
{ |
|
if ( rc.data ) |
|
{ |
|
fwrite( rc.data, rc.nBytesTotal, 1, fp ); |
|
} |
|
fclose( fp ); |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
|
|
#ifdef _WIN32 |
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Formats a string to spit out via OutputDebugString (only in debug). OutputDebugString |
|
* is threadsafe, so this should be fine. |
|
* |
|
* Since I don't want to be playing with the developer cvar in the other thread, I'll use |
|
* the presence of _DEBUG as my developer flag. |
|
*/ |
|
void Thread_DPrintf (char *fmt, ...) |
|
{ |
|
#ifdef _DEBUG |
|
va_list argptr; |
|
char msg[4096]; |
|
|
|
va_start( argptr, fmt ); |
|
Q_vsnprintf( msg, sizeof(msg), fmt, argptr ); |
|
va_end( argptr ); |
|
OutputDebugString( msg ); |
|
#endif // _DEBUG |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Convenience function to name status states for debugging |
|
*/ |
|
static const char *StateString( DWORD dwStatus ) |
|
{ |
|
switch (dwStatus) |
|
{ |
|
case INTERNET_STATUS_RESOLVING_NAME: |
|
return "INTERNET_STATUS_RESOLVING_NAME"; |
|
case INTERNET_STATUS_NAME_RESOLVED: |
|
return "INTERNET_STATUS_NAME_RESOLVED"; |
|
case INTERNET_STATUS_CONNECTING_TO_SERVER: |
|
return "INTERNET_STATUS_CONNECTING_TO_SERVER"; |
|
case INTERNET_STATUS_CONNECTED_TO_SERVER: |
|
return "INTERNET_STATUS_CONNECTED_TO_SERVER"; |
|
case INTERNET_STATUS_SENDING_REQUEST: |
|
return "INTERNET_STATUS_SENDING_REQUEST"; |
|
case INTERNET_STATUS_REQUEST_SENT: |
|
return "INTERNET_STATUS_REQUEST_SENT"; |
|
case INTERNET_STATUS_REQUEST_COMPLETE: |
|
return "INTERNET_STATUS_REQUEST_COMPLETE"; |
|
case INTERNET_STATUS_CLOSING_CONNECTION: |
|
return "INTERNET_STATUS_CLOSING_CONNECTION"; |
|
case INTERNET_STATUS_CONNECTION_CLOSED: |
|
return "INTERNET_STATUS_CONNECTION_CLOSED"; |
|
case INTERNET_STATUS_RECEIVING_RESPONSE: |
|
return "INTERNET_STATUS_RECEIVING_RESPONSE"; |
|
case INTERNET_STATUS_RESPONSE_RECEIVED: |
|
return "INTERNET_STATUS_RESPONSE_RECEIVED"; |
|
case INTERNET_STATUS_HANDLE_CLOSING: |
|
return "INTERNET_STATUS_HANDLE_CLOSING"; |
|
case INTERNET_STATUS_HANDLE_CREATED: |
|
return "INTERNET_STATUS_HANDLE_CREATED"; |
|
case INTERNET_STATUS_INTERMEDIATE_RESPONSE: |
|
return "INTERNET_STATUS_INTERMEDIATE_RESPONSE"; |
|
case INTERNET_STATUS_REDIRECT: |
|
return "INTERNET_STATUS_REDIRECT"; |
|
case INTERNET_STATUS_STATE_CHANGE: |
|
return "INTERNET_STATUS_STATE_CHANGE"; |
|
} |
|
return "Unknown"; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Callback function to update status information for a download (connecting to server, etc) |
|
*/ |
|
void __stdcall DownloadStatusCallback( HINTERNET hOpenResource, DWORD dwContext, DWORD dwStatus, LPVOID pStatusInfo, DWORD dwStatusInfoLength ) |
|
{ |
|
RequestContext_t *rc = (RequestContext_t*)pStatusInfo; |
|
|
|
switch (dwStatus) |
|
{ |
|
case INTERNET_STATUS_RESOLVING_NAME: |
|
case INTERNET_STATUS_NAME_RESOLVED: |
|
case INTERNET_STATUS_CONNECTING_TO_SERVER: |
|
case INTERNET_STATUS_CONNECTED_TO_SERVER: |
|
case INTERNET_STATUS_SENDING_REQUEST: |
|
case INTERNET_STATUS_REQUEST_SENT: |
|
case INTERNET_STATUS_REQUEST_COMPLETE: |
|
case INTERNET_STATUS_CLOSING_CONNECTION: |
|
case INTERNET_STATUS_CONNECTION_CLOSED: |
|
if ( rc ) |
|
{ |
|
rc->fetchStatus = dwStatus; |
|
} |
|
else |
|
{ |
|
//Thread_DPrintf( "** No RequestContext_t **\n" ); |
|
} |
|
//Thread_DPrintf( "DownloadStatusCallback %s\n", StateString(dwStatus) ); |
|
break; |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Reads data from a handle opened by InternetOpenUrl(). |
|
*/ |
|
void ReadData( RequestContext_t& rc ) |
|
{ |
|
const int BufferSize = 2048; |
|
unsigned char data[BufferSize]; |
|
DWORD dwSize = 0; |
|
|
|
if ( !rc.nBytesTotal ) |
|
{ |
|
rc.status = HTTP_ERROR; |
|
rc.error = HTTP_ERROR_ZERO_LENGTH_FILE; |
|
return; |
|
} |
|
rc.nBytesCurrent = rc.nBytesCached; |
|
rc.status = HTTP_FETCH; |
|
|
|
while ( !rc.shouldStop ) |
|
{ |
|
// InternetReadFile() will block until there is data, or the socket gets closed. This means the |
|
// main thread could request an abort while we're blocked here. This is okay, because the main |
|
// thread will not wait for this thread to finish, but will clean up the RequestContext_t at some |
|
// later point when InternetReadFile() has returned and this thread has finished. |
|
if ( !InternetReadFile( rc.hDataResource, (LPVOID)data, BufferSize, &dwSize ) ) |
|
{ |
|
// if InternetReadFile() returns 0, there was a socket error (connection closed, etc) |
|
rc.status = HTTP_ERROR; |
|
rc.error = HTTP_ERROR_CONNECTION_CLOSED; |
|
return; |
|
} |
|
if ( !dwSize ) |
|
{ |
|
// if InternetReadFile() succeeded, but we read 0 bytes, we're at the end of the file. |
|
|
|
// if the file doesn't exist, write it out |
|
WriteFileFromRequestContext( rc ); |
|
|
|
// Let the main thread know we finished reading data, and wait for it to let us exit. |
|
rc.status = HTTP_DONE; |
|
return; |
|
} |
|
else |
|
{ |
|
// We've read some data. Make sure we won't walk off the end of our buffer, then |
|
// use memcpy() to save the data off. |
|
DWORD safeSize = (DWORD) min( rc.nBytesTotal - rc.nBytesCurrent, (int)dwSize ); |
|
//Thread_DPrintf( "Read %d bytes @ offset %d\n", safeSize, rc.nBytesCurrent ); |
|
if ( safeSize != dwSize ) |
|
{ |
|
//Thread_DPrintf( "Warning - read more data than expected!\n" ); |
|
} |
|
if ( rc.data && safeSize > 0 ) |
|
{ |
|
memcpy( rc.data + rc.nBytesCurrent, data, safeSize ); |
|
} |
|
rc.nBytesCurrent += safeSize; |
|
} |
|
} |
|
|
|
// If we get here, rc.shouldStop was set early (user hit cancel). |
|
// Let the main thread know we aborted properly. |
|
rc.status = HTTP_ABORTED; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
const char *StatusString[] = |
|
{ |
|
"HTTP_CONNECTING", |
|
"HTTP_FETCH", |
|
"HTTP_DONE", |
|
"HTTP_ABORTED", |
|
"HTTP_ERROR", |
|
}; |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
const char *ErrorString[] = |
|
{ |
|
"HTTP_ERROR_NONE", |
|
"HTTP_ERROR_ZERO_LENGTH_FILE", |
|
"HTTP_ERROR_CONNECTION_CLOSED", |
|
"HTTP_ERROR_INVALID_URL", |
|
"HTTP_ERROR_INVALID_PROTOCOL", |
|
"HTTP_ERROR_CANT_BIND_SOCKET", |
|
"HTTP_ERROR_CANT_CONNECT", |
|
"HTTP_ERROR_NO_HEADERS", |
|
"HTTP_ERROR_FILE_NONEXISTENT", |
|
"HTTP_ERROR_MAX", |
|
}; |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Closes all open handles, and waits until the main thread has given the OK |
|
* to quit. |
|
*/ |
|
void CleanUpDownload( RequestContext_t& rc, HTTPStatus_t status, HTTPError_t error = HTTP_ERROR_NONE ) |
|
{ |
|
if ( status != HTTP_DONE || error != HTTP_ERROR_NONE ) |
|
{ |
|
//Thread_DPrintf( "CleanUpDownload() - http status is %s, error state is %s\n", |
|
// StatusString[status], ErrorString[error] ); |
|
} |
|
|
|
rc.status = status; |
|
rc.error = error; |
|
|
|
// Close all HINTERNET handles we have open |
|
if ( rc.hDataResource && !InternetCloseHandle(rc.hDataResource) ) |
|
{ |
|
//Thread_DPrintf( "Failed to close data resource for %s%s\n", rc.baseURL, rc.gamePath ); |
|
} |
|
else if ( rc.hOpenResource && !InternetCloseHandle(rc.hOpenResource) ) |
|
{ |
|
//Thread_DPrintf( "Failed to close open resource for %s%s\n", rc.baseURL, rc.gamePath ); |
|
} |
|
rc.hDataResource = NULL; |
|
rc.hOpenResource = NULL; |
|
|
|
// wait until the main thread says we can go away (so it can look at rc.data). |
|
while ( !rc.shouldStop ) |
|
{ |
|
Sleep( 100 ); |
|
} |
|
|
|
// Delete rc.data, which was allocated in this thread |
|
if ( rc.data != NULL ) |
|
{ |
|
delete[] rc.data; |
|
rc.data = NULL; |
|
} |
|
|
|
// and tell the main thread we're exiting, so it can delete rc.cachedData and rc itself. |
|
rc.threadDone = true; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
static void DumpHeaders( RequestContext_t& rc ) |
|
{ |
|
#ifdef _DEBUG |
|
DWORD dwSize; |
|
|
|
// First time we will find out the size of the headers. |
|
HttpQueryInfo ( rc.hDataResource, HTTP_QUERY_RAW_HEADERS_CRLF, NULL, &dwSize, NULL ); |
|
char *lpBuffer = new char [dwSize + 2]; |
|
|
|
// Now we call HttpQueryInfo again to get the headers. |
|
if (!HttpQueryInfo ( rc.hDataResource, HTTP_QUERY_RAW_HEADERS_CRLF, (LPVOID)lpBuffer, &dwSize, NULL)) |
|
{ |
|
return; |
|
} |
|
*(lpBuffer + dwSize) = '\n'; |
|
*(lpBuffer + dwSize + 1) = '\0'; |
|
|
|
Thread_DPrintf( "------------------------------\n%s%s\n%s------------------------------\n", |
|
rc.baseURL, rc.gamePath, lpBuffer ); |
|
#endif |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Main download thread function - implements a (partial) synchronous HTTP download. |
|
*/ |
|
DWORD __stdcall DownloadThread( void *voidPtr ) |
|
{ |
|
RequestContext_t& rc = *(RequestContext_t *)voidPtr; |
|
|
|
URL_COMPONENTS url; |
|
char urlBuf[6][BufferSize]; |
|
url.dwStructSize = sizeof(url); |
|
|
|
url.dwSchemeLength = BufferSize; |
|
url.dwHostNameLength = BufferSize; |
|
url.dwUserNameLength = BufferSize; |
|
url.dwPasswordLength = BufferSize; |
|
url.dwUrlPathLength = BufferSize; |
|
url.dwExtraInfoLength = BufferSize; |
|
|
|
url.lpszScheme = urlBuf[0]; |
|
url.lpszHostName = urlBuf[1]; |
|
url.lpszUserName = urlBuf[2]; |
|
url.lpszPassword = urlBuf[3]; |
|
url.lpszUrlPath = urlBuf[4]; |
|
url.lpszExtraInfo = urlBuf[5]; |
|
|
|
char fullURL[BufferSize*2]; |
|
DWORD fullURLLength = BufferSize*2; |
|
Q_snprintf( fullURL, fullURLLength, "%s%s", rc.baseURL, rc.urlPath ); |
|
|
|
if ( !InternetCrackUrl( fullURL, fullURLLength, 0, &url ) ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_INVALID_URL ); |
|
return rc.status; |
|
} |
|
|
|
/// @TODO: Add FTP support (including restart of aborted transfers) -MDC 2004/01/08 |
|
// We should be able to handle FTP downloads as well, but I don't have a server to check against, so |
|
// I'm gonna disallow it in case something bad would happen. |
|
if ( url.nScheme != INTERNET_SCHEME_HTTP && url.nScheme != INTERNET_SCHEME_HTTPS ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_INVALID_PROTOCOL ); |
|
return rc.status; |
|
} |
|
|
|
// Open a socket etc for the download. |
|
// The first parameter, "Half-Life", is the User-Agent that gets sent with HTTP requests. |
|
// INTERNET_OPEN_TYPE_PRECONFIG specifies using IE's proxy info from the registry for HTTP downloads. |
|
rc.hOpenResource = InternetOpen( "Half-Life 2", INTERNET_OPEN_TYPE_PRECONFIG ,NULL, NULL, 0); |
|
|
|
if ( !rc.hOpenResource ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_CANT_BIND_SOCKET ); |
|
return rc.status; |
|
} |
|
|
|
InternetSetStatusCallback( rc.hOpenResource, (INTERNET_STATUS_CALLBACK)DownloadStatusCallback ); |
|
|
|
if ( rc.shouldStop ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ABORTED ); |
|
return rc.status; |
|
} |
|
|
|
// Set up some flags |
|
DWORD flags = 0; |
|
flags |= INTERNET_FLAG_RELOAD; // Get from server, not IE's cache |
|
flags |= INTERNET_FLAG_NO_CACHE_WRITE; // Don't write to IE's cache, since we're doing our own caching of partial downloads |
|
flags |= INTERNET_FLAG_KEEP_CONNECTION; // Use keep-alive semantics. Since each file downloaded is a separate connection |
|
// from a separate thread, I don't think this does much. Can't hurt, though. |
|
if ( url.nScheme == INTERNET_SCHEME_HTTPS ) |
|
{ |
|
// The following flags allow us to use https:// URLs, but don't provide much in the way of authentication. |
|
// In other words, this allows people with only access to https:// servers (?!?) to host files as transparently |
|
// as possible. |
|
flags |= INTERNET_FLAG_SECURE; // Use SSL, etc. Kinda need this for HTTPS URLs. |
|
flags |= INTERNET_FLAG_IGNORE_CERT_CN_INVALID; // Don't check hostname on the SSL cert. |
|
flags |= INTERNET_FLAG_IGNORE_CERT_DATE_INVALID; // Don't check for expired SSL certs. |
|
} |
|
|
|
// Request a partial if we have the data |
|
char headers[BufferSize] = ""; |
|
DWORD headerLen = 0; |
|
char *headerPtr = NULL; |
|
if ( *rc.cachedTimestamp && rc.nBytesCached ) |
|
{ |
|
if ( *rc.serverURL ) |
|
{ |
|
Q_snprintf( headers, BufferSize, "If-Range: %s\nRange: bytes=%d-\nReferer: hl2://%s\n", |
|
rc.cachedTimestamp, rc.nBytesCached, rc.serverURL ); |
|
} |
|
else |
|
{ |
|
Q_snprintf( headers, BufferSize, "If-Range: %s\nRange: bytes=%d-\n", |
|
rc.cachedTimestamp, rc.nBytesCached ); |
|
} |
|
headerPtr = headers; |
|
headerLen = (DWORD)-1L; // the DWORD cast is because we get a signed/unsigned mismatch even with an L on the -1. |
|
//Thread_DPrintf( "Requesting partial download\n%s", headers ); |
|
} |
|
else if ( *rc.serverURL ) |
|
{ |
|
Q_snprintf( headers, BufferSize, "Referer: hl2://%s\n", rc.serverURL ); |
|
headerPtr = headers; |
|
headerLen = (DWORD)-1L; // the DWORD cast is because we get a signed/unsigned mismatch even with an L on the -1. |
|
//Thread_DPrintf( "Requesting full download\n%s", headers ); |
|
} |
|
|
|
rc.hDataResource = InternetOpenUrl(rc.hOpenResource, fullURL, headerPtr, headerLen, flags,(DWORD)(&rc) ); |
|
|
|
// send the request off |
|
if ( !rc.hDataResource ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_CANT_CONNECT ); |
|
return rc.status; |
|
} |
|
|
|
if ( rc.shouldStop ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ABORTED ); |
|
return rc.status; |
|
} |
|
|
|
//DumpHeaders( rc ); // debug |
|
|
|
// check the status (are we gonna get anything?) |
|
DWORD size = sizeof(DWORD); |
|
DWORD code; |
|
if ( !HttpQueryInfo( rc.hDataResource, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &code, &size, NULL ) ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_NO_HEADERS ); |
|
return rc.status; |
|
} |
|
|
|
// Only status codes we're looking for are HTTP_STATUS_OK (200) and HTTP_STATUS_PARTIAL_CONTENT (206) |
|
if ( code != HTTP_STATUS_OK && code != HTTP_STATUS_PARTIAL_CONTENT ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_FILE_NONEXISTENT ); |
|
return rc.status; |
|
} |
|
|
|
// get the timestamp, and save it off for future resumes, in case we abort this transfer later. |
|
size = BufferSize; |
|
if ( !HttpQueryInfo( rc.hDataResource, HTTP_QUERY_LAST_MODIFIED, rc.cachedTimestamp, &size, NULL ) ) |
|
{ |
|
rc.cachedTimestamp[0] = 0; |
|
} |
|
rc.cachedTimestamp[BufferSize-1] = 0; |
|
|
|
// If we're not getting a partial download, don't use any cached data, even if we have some. |
|
if ( code != HTTP_STATUS_PARTIAL_CONTENT ) |
|
{ |
|
if ( rc.nBytesCached ) |
|
{ |
|
//Thread_DPrintf( "Partial download refused - getting full version\n" ); |
|
} |
|
rc.nBytesCached = 0; // start from the beginning |
|
} |
|
|
|
// Check the resource size, and allocate a buffer |
|
size = sizeof(code); |
|
if ( HttpQueryInfo( rc.hDataResource, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &code, &size, NULL ) ) |
|
{ |
|
rc.nBytesTotal = code + rc.nBytesCached; |
|
if ( code > 0 ) |
|
{ |
|
rc.data = new unsigned char[rc.nBytesTotal + 1]; // Extra byte for NULL terminator |
|
if ( !rc.data ) |
|
{ |
|
// We're seeing crazy large numbers being returned in code (0x48e1e22), the new fails, and we crash. |
|
// This should probably be a different error? |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_ZERO_LENGTH_FILE ); |
|
return rc.status; |
|
} |
|
rc.data[ rc.nBytesTotal ] = 0; |
|
} |
|
} |
|
else |
|
{ |
|
CleanUpDownload( rc, HTTP_ERROR, HTTP_ERROR_ZERO_LENGTH_FILE ); |
|
return rc.status; |
|
} |
|
|
|
// copy cached data into buffer |
|
if ( rc.cacheData && rc.nBytesCached ) |
|
{ |
|
int len = min( rc.nBytesCached, rc.nBytesTotal ); |
|
memcpy( rc.data, rc.cacheData, len ); |
|
} |
|
|
|
if ( rc.shouldStop ) |
|
{ |
|
CleanUpDownload( rc, HTTP_ABORTED ); |
|
return rc.status; |
|
} |
|
|
|
// now download the actual data |
|
ReadData( rc ); |
|
|
|
// and stick around until the main thread has gotten the data. |
|
CleanUpDownload( rc, rc.status, rc.error ); |
|
return rc.status; |
|
} |
|
|
|
|
|
#elif defined( POSIX ) |
|
|
|
#include "curl/curl.h" |
|
|
|
// curl callback functions |
|
|
|
static size_t curlWriteFn( void *ptr, size_t size, size_t nmemb, void *stream) |
|
{ |
|
RequestContext_t *pRC = (RequestContext_t *) stream; |
|
if ( pRC->nBytesTotal && pRC->nBytesCurrent + ( size * nmemb ) <= pRC->nBytesTotal ) |
|
{ |
|
Q_memcpy( pRC->data + pRC->nBytesCurrent, ptr, ( size * nmemb ) ); |
|
pRC->nBytesCurrent += size * nmemb; |
|
} |
|
return size * nmemb; |
|
} |
|
|
|
|
|
int Q_StrTrim( char *pStr ) |
|
{ |
|
char *pSource = pStr; |
|
char *pDest = pStr; |
|
|
|
// skip white space at the beginning |
|
while ( *pSource != 0 && isspace( *pSource ) ) |
|
{ |
|
pSource++; |
|
} |
|
|
|
// copy everything else |
|
char *pLastWhiteBlock = NULL; |
|
char *pStart = pDest; |
|
while ( *pSource != 0 ) |
|
{ |
|
*pDest = *pSource++; |
|
if ( isspace( *pDest ) ) |
|
{ |
|
if ( pLastWhiteBlock == NULL ) |
|
pLastWhiteBlock = pDest; |
|
} |
|
else |
|
{ |
|
pLastWhiteBlock = NULL; |
|
} |
|
pDest++; |
|
} |
|
*pDest = 0; |
|
|
|
// did we end in a whitespace block? |
|
if ( pLastWhiteBlock != NULL ) |
|
{ |
|
// yep; shorten the string |
|
pDest = pLastWhiteBlock; |
|
*pLastWhiteBlock = 0; |
|
} |
|
|
|
return pDest - pStart; |
|
} |
|
|
|
static size_t curlHeaderFn( void *ptr, size_t size, size_t nmemb, void *stream) |
|
{ |
|
char *pszHeader = (char*)ptr; |
|
char *pszValue = NULL; |
|
RequestContext_t *pRC = (RequestContext_t *) stream; |
|
|
|
pszHeader[ ( size * nmemb - 1 ) ] = NULL; |
|
pszValue = Q_strstr( pszHeader, ":" ); |
|
if ( pszValue ) |
|
{ |
|
// null terminate the header name, and point pszValue at it's value |
|
*pszValue = NULL; |
|
pszValue++; |
|
Q_StrTrim( pszValue ); |
|
} |
|
if ( 0 == Q_stricmp( pszHeader, "Content-Length" ) ) |
|
{ |
|
size_t len = atol( pszValue ); |
|
if ( pRC && len ) |
|
{ |
|
pRC->nBytesTotal = len; |
|
pRC->data = (byte*)malloc( len ); |
|
} |
|
} |
|
|
|
return size * nmemb; |
|
} |
|
|
|
|
|
// we're going to abuse this by using it for proxy pac fetching |
|
// the cacheData field will hold the URL of the PAC, and the data |
|
// field the contents of the pac |
|
RequestContext_t g_pacRequestCtx; |
|
|
|
// system specific headers for proxy configuration |
|
#if defined(OSX) |
|
#include <CoreFoundation/CoreFoundation.h> |
|
#include <CoreServices/CoreServices.h> |
|
#include <SystemConfiguration/SystemConfiguration.h> |
|
#endif |
|
|
|
|
|
void SetProxiesForURL( CURL *hMasterCURL, const char *pszURL ) |
|
{ |
|
uint32 uProxyPort = 0; |
|
char rgchProxyHost[1024]; |
|
char *pszProxyExceptionList = NULL; |
|
rgchProxyHost[0] = '\0'; |
|
|
|
#if defined(OSX) |
|
|
|
// create an urlref around the raw URL |
|
CFURLRef url = CFURLCreateWithBytes( NULL, ( const UInt8 * ) pszURL, strlen( pszURL ), kCFStringEncodingASCII, NULL ); |
|
// copy the proxies dictionary |
|
CFDictionaryRef proxyDict = SCDynamicStoreCopyProxies(NULL); |
|
// and ask the system what proxies it thinks I should consider for the given URL |
|
CFArrayRef proxies = CFNetworkCopyProxiesForURL( url, proxyDict ); |
|
|
|
CFIndex iProxy; |
|
// walk through the returned set, looking for any types we (and lib curl) can handle |
|
// the list is returned in "preference order", but we can only handle http, and pac urls |
|
for( iProxy = 0; iProxy < CFArrayGetCount( proxies ); iProxy++ ) |
|
{ |
|
CFDictionaryRef proxy = (CFDictionaryRef) CFArrayGetValueAtIndex( proxies, iProxy ); |
|
|
|
if ( proxy == NULL ) |
|
break; |
|
|
|
// what type of proxy is this one? |
|
CFStringRef proxyType = (CFStringRef) CFDictionaryGetValue( proxy, kCFProxyTypeKey ); |
|
|
|
if ( CFEqual( proxyType, kCFProxyTypeNone ) ) |
|
{ |
|
// no proxy should be used - we're done. |
|
break; |
|
} |
|
else if ( CFEqual( proxyType, kCFProxyTypeHTTP ) ) |
|
{ |
|
// manually configured HTTP proxy settings. |
|
const void *val = NULL; |
|
|
|
// grab the proxy port |
|
val = CFDictionaryGetValue( proxy, kCFProxyPortNumberKey ); |
|
if ( val == NULL || !CFNumberGetValue( (CFNumberRef) val, kCFNumberIntType, &uProxyPort ) ) |
|
// either we failed, or the port was invalid |
|
continue; |
|
|
|
// no port specified - use the default http port |
|
if ( uProxyPort == 0 ) |
|
uProxyPort = 80; |
|
|
|
int cbOffset = 0; |
|
// see if they've specified authentication (username/password) |
|
val = CFDictionaryGetValue( proxy, kCFProxyUsernameKey ); |
|
if ( val != NULL && CFStringGetCString( (CFStringRef) val, rgchProxyHost + cbOffset, sizeof(rgchProxyHost) - cbOffset, kCFStringEncodingASCII ) && rgchProxyHost[cbOffset] != '\0' ) |
|
{ |
|
// we've got "username" in rgchProxyHost |
|
cbOffset = Q_strlen( rgchProxyHost ); |
|
val = CFDictionaryGetValue( proxy, kCFProxyPasswordKey ); |
|
if ( val != NULL && CFStringGetLength( (CFStringRef) val ) ) |
|
{ |
|
// and there's a non-null password value - put a colon after username |
|
rgchProxyHost[cbOffset++] = ':'; |
|
CFStringGetCString( (CFStringRef) val, rgchProxyHost + cbOffset, sizeof(rgchProxyHost) - cbOffset, kCFStringEncodingASCII ); |
|
// now we've got user:password in rgchProxyHost |
|
cbOffset = Q_strlen( rgchProxyHost ); |
|
} |
|
// since we've got at least a username, we need an @ |
|
rgchProxyHost[cbOffset++] = '@'; |
|
} |
|
|
|
val = CFDictionaryGetValue( proxy, kCFProxyHostNameKey ); |
|
if ( val == NULL || !CFStringGetCString( (CFStringRef) val, rgchProxyHost + cbOffset, sizeof(rgchProxyHost) - cbOffset, kCFStringEncodingASCII ) || rgchProxyHost[cbOffset] == '\0' ) |
|
continue; |
|
|
|
break; |
|
} |
|
else if ( CFEqual( proxyType, kCFProxyTypeAutoConfigurationURL ) ) |
|
{ |
|
// a proxy autoconfig URL has been provided |
|
char rgchPacURL[1024]; |
|
// get the url (as an urlref) and turn it into a string |
|
CFURLRef cfUrl = (CFURLRef) CFDictionaryGetValue( proxy, kCFProxyAutoConfigurationURLKey ); |
|
CFStringGetCString( (CFStringRef) CFStringCreateWithFormat( NULL, NULL, CFSTR("%@"), cfUrl ), |
|
rgchPacURL, sizeof( rgchPacURL ), kCFStringEncodingASCII ); |
|
|
|
CURLcode res = CURLE_OK; |
|
// see if we've not yet fetched this pac file |
|
if ( !g_pacRequestCtx.cacheData || Q_strcmp( (const char *)g_pacRequestCtx.cacheData, rgchPacURL ) ) |
|
{ |
|
if ( g_pacRequestCtx.cacheData ) |
|
{ |
|
free( g_pacRequestCtx.cacheData ); |
|
g_pacRequestCtx.cacheData = NULL; |
|
} |
|
if ( g_pacRequestCtx.data ) |
|
{ |
|
free( g_pacRequestCtx.data ); |
|
g_pacRequestCtx.data = NULL; |
|
} |
|
|
|
// grab the data, using the same request context structure (and callbacks) we use for real downloads |
|
CURL *hCURL = curl_easy_init(); |
|
if ( !hCURL ) |
|
{ |
|
AssertMsg( hCURL, "failed to initialize curl handle" ); |
|
break; |
|
} |
|
curl_easy_setopt( hCURL, CURLOPT_NOPROGRESS, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_NOSIGNAL, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_CONNECTTIMEOUT, 30 ); |
|
curl_easy_setopt( hCURL, CURLOPT_FOLLOWLOCATION, 1 ); // follow 30x redirections from the web server |
|
|
|
// and setup the callback fns |
|
curl_easy_setopt( hCURL, CURLOPT_HEADERFUNCTION, &curlHeaderFn ); |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEFUNCTION, &curlWriteFn ); |
|
|
|
// setup callback stream pointers |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEHEADER, &g_pacRequestCtx ); |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEDATA, &g_pacRequestCtx ); |
|
|
|
curl_easy_setopt( hCURL, CURLOPT_URL, rgchPacURL ); |
|
|
|
res = curl_easy_perform( hCURL ); |
|
curl_easy_cleanup( hCURL ); |
|
} |
|
if ( res == CURLE_OK ) |
|
{ |
|
// copy the URL into the "pac cache", if necessary |
|
if ( !g_pacRequestCtx.cacheData ) |
|
{ |
|
g_pacRequestCtx.cacheData = (unsigned char*) malloc( Q_strlen( rgchPacURL ) + 1 ); |
|
Q_memcpy( g_pacRequestCtx.cacheData, rgchPacURL, Q_strlen( rgchPacURL ) ); |
|
} |
|
|
|
if ( !g_pacRequestCtx.data ) // no data in the proxy.pac they have, so just ignore it |
|
return; |
|
|
|
// wrap the data (the pac contents) into a cfstring |
|
CFStringRef cfPacStr = CFStringCreateWithCString( kCFAllocatorDefault, (const char *)g_pacRequestCtx.data, kCFStringEncodingASCII ); |
|
|
|
// and ask the system, given this proxy pac, what (list of) proxies should I consider for this URL? |
|
CFErrorRef err; |
|
CFArrayRef proxiesForUrl = CFNetworkCopyProxiesForAutoConfigurationScript( cfPacStr, cfUrl, &err ); |
|
if ( proxiesForUrl ) |
|
{ |
|
// we're re-assigning the value that the loop is iterating over, the postincrement will fire after we do this, |
|
// hence the -1 (rather than 0) assignment to iProxy |
|
proxies = proxiesForUrl; |
|
iProxy = -1; |
|
} |
|
continue; |
|
} |
|
else |
|
{ |
|
if ( g_pacRequestCtx.cacheData ) |
|
{ |
|
free( g_pacRequestCtx.cacheData ); |
|
g_pacRequestCtx.cacheData = NULL; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
Msg( "unsupported proxy type\n" ); |
|
break; |
|
} |
|
} |
|
#else |
|
#warning "CHTTPDownloadThread doesn't know how to set proxy config" |
|
#endif |
|
|
|
if ( rgchProxyHost[0] == '\0' || uProxyPort <= 0 ) |
|
{ |
|
if ( pszProxyExceptionList ) |
|
free( pszProxyExceptionList ); |
|
return; |
|
} |
|
|
|
curl_easy_setopt( hMasterCURL, CURLOPT_PROXY, rgchProxyHost ); |
|
curl_easy_setopt( hMasterCURL, CURLOPT_PROXYPORT, uProxyPort ); |
|
if ( pszProxyExceptionList ) |
|
{ |
|
curl_easy_setopt( hMasterCURL, (CURLoption) (CURLOPTTYPE_OBJECTPOINT + 177) /*CURLOPT_NOPROXY*/ , pszProxyExceptionList ); |
|
free( pszProxyExceptionList ); |
|
} |
|
|
|
} |
|
|
|
|
|
void DownloadThread( void *voidPtr ) |
|
{ |
|
static bool bDoneInit = false; |
|
if ( !bDoneInit ) |
|
{ |
|
bDoneInit = true; |
|
curl_global_init( CURL_GLOBAL_SSL ); |
|
} |
|
|
|
RequestContext_t& rc = *(RequestContext_t *)voidPtr; |
|
|
|
rc.status = HTTP_FETCH; |
|
|
|
CURL *hCURL = curl_easy_init(); |
|
if ( !hCURL ) |
|
{ |
|
rc.error = HTTP_ERROR_INVALID_URL; |
|
rc.status = HTTP_ERROR; |
|
rc.threadDone = true; |
|
return; |
|
} |
|
|
|
curl_easy_setopt( hCURL, CURLOPT_NOPROGRESS, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_NOSIGNAL, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_CONNECTTIMEOUT, 30 ); |
|
curl_easy_setopt( hCURL, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); |
|
|
|
// turn off certificate verification similar to how we set INTERNET_FLAG_IGNORE_CERT_CN_INVALID and INTERNET_FLAG_IGNORE_CERT_DATE_INVALID on Windows |
|
curl_easy_setopt( hCURL, CURLOPT_SSL_VERIFYHOST, 0 ); |
|
curl_easy_setopt( hCURL, CURLOPT_SSL_VERIFYPEER, 0 ); |
|
|
|
// and now the callback fns |
|
curl_easy_setopt( hCURL, CURLOPT_HEADERFUNCTION, &curlHeaderFn ); |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEFUNCTION, &curlWriteFn ); |
|
|
|
|
|
uint32 cubURL = Q_strlen( rc.baseURL ) + Q_strlen( rc.urlPath ) + 2 /*one for the /, one for the null*/; |
|
char *pchURL = (char *) malloc( cubURL ); |
|
Q_snprintf( pchURL, cubURL, "%s%s", rc.baseURL, rc.urlPath ); |
|
|
|
uint32 cubReferer = 0; |
|
char *pchReferer = NULL; |
|
if ( *rc.serverURL ) |
|
{ |
|
cubReferer = Q_strlen( rc.serverURL ) + 8; |
|
pchReferer = (char *) malloc( cubReferer ); |
|
Q_snprintf( pchReferer, cubURL, "hl2://%s", rc.serverURL ); |
|
} |
|
|
|
// setup proxies |
|
SetProxiesForURL( hCURL, pchURL ); |
|
|
|
// set the url |
|
curl_easy_setopt( hCURL, CURLOPT_URL, pchURL ); |
|
|
|
// setup callback stream pointers |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEHEADER, &rc ); |
|
curl_easy_setopt( hCURL, CURLOPT_WRITEDATA, &rc ); |
|
|
|
curl_easy_setopt( hCURL, CURLOPT_FOLLOWLOCATION, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_MAXREDIRS, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_UNRESTRICTED_AUTH, 1 ); |
|
curl_easy_setopt( hCURL, CURLOPT_USERAGENT, "Half-Life 2" ); |
|
if ( pchReferer ) |
|
{ |
|
curl_easy_setopt( hCURL, CURLOPT_REFERER, pchReferer ); |
|
} |
|
|
|
|
|
// g0g0g0 |
|
CURLcode res = curl_easy_perform( hCURL ); |
|
|
|
free( pchURL ); |
|
if ( pchReferer ) |
|
{ |
|
free( pchReferer ); |
|
} |
|
|
|
if ( res == CURLE_OK ) |
|
{ |
|
curl_easy_getinfo( hCURL , CURLINFO_RESPONSE_CODE , &rc.status ); |
|
if ( rc.status == HTTPStatus_t(200) || rc.status == HTTPStatus_t(206) ) |
|
{ |
|
// write the file before we change the status to DONE, so that the write |
|
// will finish before the main thread goes on and starts messing with the file |
|
WriteFileFromRequestContext( rc ); |
|
|
|
rc.status = HTTP_DONE; |
|
rc.error = HTTP_ERROR_NONE; |
|
} |
|
else |
|
{ |
|
rc.status = HTTP_ERROR; |
|
rc.error = HTTP_ERROR_FILE_NONEXISTENT; |
|
} |
|
} |
|
else |
|
{ |
|
rc.status = HTTP_ERROR; |
|
} |
|
|
|
// wait until the main thread says we can go away (so it can look at rc.data). |
|
while ( !rc.shouldStop ) |
|
{ |
|
ThreadSleep( 100 ); |
|
} |
|
|
|
// Delete rc.data, which was allocated in this thread |
|
if ( rc.data != NULL ) |
|
{ |
|
delete[] rc.data; |
|
rc.data = NULL; |
|
} |
|
|
|
|
|
curl_easy_cleanup( hCURL ); |
|
|
|
rc.threadDone = true; |
|
} |
|
#else |
|
void DownloadThread( void *voidPtr ) |
|
{ |
|
#warning "DownloadThread Not Implemented" |
|
Assert( !"Implement me" ); |
|
} |
|
#endif |
|
|
|
|