summaryrefslogtreecommitdiff
path: root/engine/pure_server.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /engine/pure_server.cpp
downloadarchived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.tar.xz
archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.zip
Diffstat (limited to 'engine/pure_server.cpp')
-rw-r--r--engine/pure_server.cpp1145
1 files changed, 1145 insertions, 0 deletions
diff --git a/engine/pure_server.cpp b/engine/pure_server.cpp
new file mode 100644
index 0000000..b645ddf
--- /dev/null
+++ b/engine/pure_server.cpp
@@ -0,0 +1,1145 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $Workfile: $
+// $Date: $
+// $NoKeywords: $
+//=============================================================================//
+
+#include "utlstring.h"
+#include "checksum_crc.h"
+#include "userid.h"
+#include "pure_server.h"
+#include "common.h"
+#include "tier1/KeyValues.h"
+#include "convar.h"
+#include "filesystem_engine.h"
+#include "server.h"
+#include "sv_filter.h"
+#include "tier1/UtlSortVector.h"
+
+// NOTE: This has to be the last file included!
+#include "tier0/memdbgon.h"
+
+extern ConVar sv_pure_consensus;
+extern ConVar sv_pure_retiretime;
+extern ConVar sv_pure_trace;
+
+CPureServerWhitelist::CCommand::CCommand()
+{
+}
+
+CPureServerWhitelist::CCommand::~CCommand()
+{
+}
+
+
+CPureServerWhitelist* CPureServerWhitelist::Create( IFileSystem *pFileSystem )
+{
+ CPureServerWhitelist *pRet = new CPureServerWhitelist;
+ pRet->Init( pFileSystem );
+ return pRet;
+}
+
+
+CPureServerWhitelist::CPureServerWhitelist()
+{
+ m_pFileSystem = NULL;
+ m_LoadCounter = 0;
+ m_RefCount = 1;
+}
+
+
+CPureServerWhitelist::~CPureServerWhitelist()
+{
+ Term();
+}
+
+
+void CPureServerWhitelist::Init( IFileSystem *pFileSystem )
+{
+ Term();
+ m_pFileSystem = pFileSystem;
+}
+
+void CPureServerWhitelist::Load( int iPureMode )
+{
+ Term();
+
+ // Not pure at all?
+ if ( iPureMode < 0 )
+ return;
+
+ // Load base trusted keys
+ {
+ KeyValues *kv = new KeyValues( "" );
+ bool bLoaded = kv->LoadFromFile( g_pFileSystem, "cfg/trusted_keys_base.txt", "game" );
+ if ( bLoaded )
+ bLoaded = LoadTrustedKeysFromKeyValues( kv );
+ else
+ Warning( "Error loading cfg/trusted_keys_base.txt\n" );
+ kv->deleteThis();
+ }
+
+ // sv_pure 0: minimal rules only
+ if ( iPureMode == 0 )
+ {
+ KeyValues *kv = new KeyValues( "" );
+ bool bLoaded = kv->LoadFromFile( g_pFileSystem, "cfg/pure_server_minimal.txt", "game" );
+ if ( bLoaded )
+ bLoaded = LoadCommandsFromKeyValues( kv );
+ else
+ Warning( "Error loading cfg/pure_server_minimal.txt\n" );
+ kv->deleteThis();
+ return;
+ }
+
+ // Load up full pure rules
+ {
+ KeyValues *kv = new KeyValues( "" );
+ bool bLoaded = kv->LoadFromFile( g_pFileSystem, "cfg/pure_server_full.txt", "game" );
+ if ( bLoaded )
+ bLoaded = LoadCommandsFromKeyValues( kv );
+ else
+ Warning( "Error loading cfg/pure_server_full.txt\n" );
+ kv->deleteThis();
+ }
+
+ // Now load user customizations
+ if ( iPureMode == 1 )
+ {
+
+ // Load custom whitelist
+ KeyValues *kv = new KeyValues( "" );
+ bool bLoaded = kv->LoadFromFile( g_pFileSystem, "cfg/pure_server_whitelist.txt", "game" );
+ if ( !bLoaded )
+ // Check the old location
+ bLoaded = kv->LoadFromFile( g_pFileSystem, "pure_server_whitelist.txt", "game" );
+ if ( bLoaded )
+ bLoaded = LoadCommandsFromKeyValues( kv );
+ else
+ Msg( "pure_server_whitelist.txt not present; pure server using only base file rules\n" );
+ kv->deleteThis();
+
+ // Load custom trusted keys
+ kv = new KeyValues( "" );
+ bLoaded = kv->LoadFromFile( g_pFileSystem, "cfg/trusted_keys.txt", "game" );
+ if ( bLoaded )
+ bLoaded = LoadTrustedKeysFromKeyValues( kv );
+ else
+ Msg( "trusted_keys.txt not present; pure server using only base trusted key list\n" );
+ kv->deleteThis();
+ }
+
+ // Hardcoded rules last
+ AddHardcodedFileCommands();
+}
+
+bool operator==( const PureServerPublicKey_t &a, const PureServerPublicKey_t &b )
+{
+ return a.Count() == b.Count() && V_memcmp( a.Base(), b.Base(), a.Count() ) == 0;
+}
+
+bool CPureServerWhitelist::CommandDictDifferent( const CUtlDict<CCommand*,int> &a, const CUtlDict<CCommand*,int> &b )
+{
+ FOR_EACH_DICT( a, idxA )
+ {
+ if ( a[idxA]->m_LoadOrder == kLoadOrder_HardcodedOverride )
+ continue;
+ int idxB = b.Find( a.GetElementName( idxA ) );
+ if ( !b.IsValidIndex( idxB ) )
+ return true;
+ if ( b[idxB]->m_LoadOrder == kLoadOrder_HardcodedOverride )
+ continue;
+ if ( a[idxA]->m_eFileClass != b[idxB]->m_eFileClass
+ || a[idxA]->m_LoadOrder != b[idxB]->m_LoadOrder )
+ return true;
+ }
+
+ return false;
+}
+
+bool CPureServerWhitelist::operator==( const CPureServerWhitelist &x ) const
+{
+
+ // Compare rule dictionaries
+ if ( CommandDictDifferent( m_FileCommands, x.m_FileCommands )
+ || CommandDictDifferent( x.m_FileCommands, m_FileCommands )
+ || CommandDictDifferent( m_RecursiveDirCommands, x.m_RecursiveDirCommands )
+ || CommandDictDifferent( x.m_RecursiveDirCommands, m_RecursiveDirCommands )
+ || CommandDictDifferent( m_NonRecursiveDirCommands, x.m_NonRecursiveDirCommands )
+ || CommandDictDifferent( x.m_NonRecursiveDirCommands, m_NonRecursiveDirCommands ) )
+ return false;
+
+ // Compare trusted key list
+ if ( m_vecTrustedKeys.Count() != x.m_vecTrustedKeys.Count() )
+ return false;
+ for ( int i = 0 ; i < m_vecTrustedKeys.Count() ; ++i )
+ if ( !( m_vecTrustedKeys[i] == x.m_vecTrustedKeys[i] ) )
+ return false;
+
+ // they are the same
+ return true;
+}
+
+void CPureServerWhitelist::Term()
+{
+ m_FileCommands.PurgeAndDeleteElements();
+ m_RecursiveDirCommands.PurgeAndDeleteElements();
+ m_NonRecursiveDirCommands.PurgeAndDeleteElements();
+ m_vecTrustedKeys.Purge();
+ m_LoadCounter = 0;
+}
+
+
+bool CPureServerWhitelist::LoadCommandsFromKeyValues( KeyValues *kv )
+{
+ for ( KeyValues *pCurItem = kv->GetFirstValue(); pCurItem; pCurItem = pCurItem->GetNextValue() )
+ {
+ char szPathName[ MAX_PATH ];
+ const char *pKeyValue = pCurItem->GetName();
+ const char *pModifiers = pCurItem->GetString();
+ if ( !pKeyValue || !pModifiers )
+ continue;
+
+ Q_strncpy( szPathName, pKeyValue, sizeof(szPathName) );
+ Q_FixSlashes( szPathName );
+ const char *pValue = szPathName;
+
+ // Figure out the modifiers.
+ bool bFromTrustedSource = false, bAllowFromDisk = false, bCheckCRC = false, bAny = false;
+ CUtlVector<char*> mods;
+ V_SplitString( pModifiers, "+", mods );
+ for ( int i=0; i < mods.Count(); i++ )
+ {
+ if (
+ V_stricmp( mods[i], "from_steam" ) == 0
+ || V_stricmp( mods[i], "trusted_source" ) == 0
+ )
+ bFromTrustedSource = true;
+ else if ( V_stricmp( mods[i], "allow_from_disk" ) == 0 )
+ bAllowFromDisk = true;
+ else if (
+ V_stricmp( mods[i], "check_crc" ) == 0
+ || V_stricmp( mods[i], "check_hash" ) == 0
+ )
+ bCheckCRC = true;
+ else if ( V_stricmp( mods[i], "any" ) == 0 )
+ bAny = true;
+ else
+ Warning( "Unknown modifier in whitelist file: %s.\n", mods[i] );
+ }
+ mods.PurgeAndDeleteElements();
+ if (
+ ( bFromTrustedSource && ( bAllowFromDisk || bCheckCRC || bAny ) )
+ || ( bAny && bCheckCRC ) )
+ {
+ Warning( "Whitelist: incompatible flags used on %s.\n", pValue );
+ continue;
+ }
+
+ EPureServerFileClass eFileClass;
+ if ( bCheckCRC )
+ eFileClass = ePureServerFileClass_CheckHash;
+ else if ( bFromTrustedSource )
+ eFileClass = ePureServerFileClass_AnyTrusted;
+ else
+ eFileClass = ePureServerFileClass_Any;
+
+ // Setup a new CCommand to hold this command.
+ AddFileCommand( pValue, eFileClass, m_LoadCounter++ );
+ }
+
+ return true;
+}
+
+void CPureServerWhitelist::AddHardcodedFileCommands()
+{
+ AddFileCommand( "materials/vgui/replay/thumbnails/...", ePureServerFileClass_Any, kLoadOrder_HardcodedOverride );
+ AddFileCommand( "sound/ui/hitsound.wav", ePureServerFileClass_Any, kLoadOrder_HardcodedOverride );
+ AddFileCommand( "sound/ui/killsound.wav", ePureServerFileClass_Any, kLoadOrder_HardcodedOverride );
+ AddFileCommand( "materials/vgui/logos/...", ePureServerFileClass_Any, kLoadOrder_HardcodedOverride );
+}
+
+void CPureServerWhitelist::AddFileCommand( const char *pszFilePath, EPureServerFileClass eFileClass, unsigned short nLoadOrder )
+{
+
+ CPureServerWhitelist::CCommand *pCommand = new CPureServerWhitelist::CCommand;
+ pCommand->m_LoadOrder = nLoadOrder;
+ pCommand->m_eFileClass = eFileClass;
+
+ // Figure out if they're referencing a file, a recursive directory, or a nonrecursive directory.
+ CUtlDict<CCommand*,int> *pList;
+ const char *pEndPart = V_UnqualifiedFileName( pszFilePath );
+ if ( Q_stricmp( pEndPart, "..." ) == 0 )
+ pList = &m_RecursiveDirCommands;
+ else if ( Q_stricmp( pEndPart, "*.*" ) == 0 )
+ pList = &m_NonRecursiveDirCommands;
+ else
+ pList = &m_FileCommands;
+
+ // If it's a directory command, get rid of the *.* or ...
+ char filePath[MAX_PATH];
+ if ( pList == &m_RecursiveDirCommands || pList == &m_NonRecursiveDirCommands )
+ V_ExtractFilePath( pszFilePath, filePath, sizeof( filePath ) );
+ else
+ V_strncpy( filePath, pszFilePath, sizeof( filePath ) );
+
+ V_FixSlashes( filePath );
+
+ int idxExisting = pList->Find( filePath );
+ if ( idxExisting != pList->InvalidIndex() )
+ {
+ delete pList->Element( idxExisting );
+ pList->RemoveAt( idxExisting );
+ }
+ pList->Insert( filePath, pCommand );
+}
+
+bool CPureServerWhitelist::LoadTrustedKeysFromKeyValues( KeyValues *kv )
+{
+ for ( KeyValues *pCurItem = kv->GetFirstTrueSubKey(); pCurItem; pCurItem = pCurItem->GetNextTrueSubKey() )
+ {
+ if ( V_stricmp( pCurItem->GetName(), "public_key" ) != 0 )
+ {
+ Warning( "Trusted key list has unexpected block '%s'; expected only 'public_key' blocks\n", pCurItem->GetName() );
+ continue;
+ }
+
+ const char *pszType = pCurItem->GetString( "type", "(none)" );
+ if ( V_stricmp( pszType, "rsa" ) != 0 )
+ {
+ Warning( "Trusted key type '%s' not supported.\n", pszType );
+ continue;
+ }
+
+ const char *pszKeyData = pCurItem->GetString( "rsa_public_key", "" );
+ if ( *pszKeyData == '\0' )
+ {
+ Warning( "trusted key is missing 'rsa_public_key' data; ignored\n" );
+ continue;
+ }
+
+ PureServerPublicKey_t &key = m_vecTrustedKeys[ m_vecTrustedKeys.AddToTail() ];
+ int nKeyDataLen = V_strlen( pszKeyData );
+ key.SetSize( nKeyDataLen / 2 );
+ // Aaaannnnnnnnddddd V_hextobinary has no return code.
+ // Because nobody could *ever* possible attempt to parse bad data. It could never possibly happen.
+ V_hextobinary( pszKeyData, nKeyDataLen, key.Base(), key.Count() );
+ }
+
+ return true;
+}
+
+void CPureServerWhitelist::UpdateCommandStats( CUtlDict<CPureServerWhitelist::CCommand*,int> &commands, int *pHighest, int *pLongestPathName )
+{
+ for ( int i=commands.First(); i != commands.InvalidIndex(); i=commands.Next( i ) )
+ {
+ *pHighest = max( *pHighest, (int)commands[i]->m_LoadOrder );
+
+ int len = V_strlen( commands.GetElementName( i ) );
+ *pLongestPathName = max( *pLongestPathName, len );
+ }
+}
+
+void CPureServerWhitelist::PrintCommand( const char *pFileSpec, const char *pExt, int maxPathnameLen, CPureServerWhitelist::CCommand *pCommand )
+{
+ // Get rid of the trailing slash if there is one.
+ char tempFileSpec[MAX_PATH];
+ V_strncpy( tempFileSpec, pFileSpec, sizeof( tempFileSpec ) );
+ int len = V_strlen( tempFileSpec );
+ if ( len > 0 && (tempFileSpec[len-1] == '/' || tempFileSpec[len-1] == '\\') )
+ tempFileSpec[len-1] = 0;
+
+ CUtlString buf;
+ if ( pExt )
+ buf.Format( "%s%c%s", tempFileSpec, CORRECT_PATH_SEPARATOR, pExt );
+ else
+ buf.Format( "%s", tempFileSpec );
+
+ len = V_strlen( pFileSpec );
+ for ( int i=len; i < maxPathnameLen+6; i++ )
+ {
+ buf += " ";
+ }
+
+ buf += "\t";
+ switch ( pCommand->m_eFileClass )
+ {
+ default:
+ buf += va( "(bogus value %d)", (int)pCommand->m_eFileClass );
+ Assert( false );
+ break;
+
+ case ePureServerFileClass_Any:
+ buf += "any";
+ break;
+
+ case ePureServerFileClass_AnyTrusted:
+ buf += "trusted_source";
+ break;
+
+ case ePureServerFileClass_CheckHash:
+ buf += "check_crc";
+ break;
+ }
+
+ Msg( "%s\n", buf.String() );
+}
+
+
+int CPureServerWhitelist::FindCommandByLoadOrder( CUtlDict<CPureServerWhitelist::CCommand*,int> &commands, int iLoadOrder )
+{
+ for ( int i=commands.First(); i != commands.InvalidIndex(); i=commands.Next( i ) )
+ {
+ if ( commands[i]->m_LoadOrder == iLoadOrder )
+ return i;
+ }
+ return -1;
+}
+
+
+void CPureServerWhitelist::PrintWhitelistContents()
+{
+ int highestLoadOrder = 0, longestPathName = 0;
+ UpdateCommandStats( m_FileCommands, &highestLoadOrder, &longestPathName );
+ UpdateCommandStats( m_RecursiveDirCommands, &highestLoadOrder, &longestPathName );
+ UpdateCommandStats( m_NonRecursiveDirCommands, &highestLoadOrder, &longestPathName );
+
+ for ( int iLoadOrder=0; iLoadOrder <= highestLoadOrder; iLoadOrder++ )
+ {
+ // Check regular file commands.
+ int iCommand = FindCommandByLoadOrder( m_FileCommands, iLoadOrder );
+ if ( iCommand != -1 )
+ {
+ PrintCommand( m_FileCommands.GetElementName( iCommand ), NULL, longestPathName, m_FileCommands[iCommand] );
+ }
+ else
+ {
+ // Check recursive commands.
+ iCommand = FindCommandByLoadOrder( m_RecursiveDirCommands, iLoadOrder );
+ if ( iCommand != -1 )
+ {
+ PrintCommand( m_RecursiveDirCommands.GetElementName( iCommand ), "...", longestPathName, m_RecursiveDirCommands[iCommand] );
+ }
+ else
+ {
+ // Check *.* commands.
+ iCommand = FindCommandByLoadOrder( m_NonRecursiveDirCommands, iLoadOrder );
+ if ( iCommand != -1 )
+ {
+ PrintCommand( m_NonRecursiveDirCommands.GetElementName( iCommand ), "*.*", longestPathName, m_NonRecursiveDirCommands[iCommand] );
+ }
+ }
+ }
+ }
+}
+
+
+void CPureServerWhitelist::Encode( CUtlBuffer &buf )
+{
+ // Put dummy version number
+ buf.PutUnsignedInt( 0xffff );
+
+ // Encode rules
+ EncodeCommandList( m_FileCommands, buf );
+ EncodeCommandList( m_RecursiveDirCommands, buf );
+ EncodeCommandList( m_NonRecursiveDirCommands, buf );
+
+ // Encode trusted keys
+ buf.PutUnsignedInt( m_vecTrustedKeys.Count() );
+ FOR_EACH_VEC( m_vecTrustedKeys, i )
+ {
+ uint32 nKeySize = m_vecTrustedKeys[i].Count();
+ buf.PutUnsignedInt( nKeySize );
+ buf.Put( m_vecTrustedKeys[i].Base(), nKeySize );
+ }
+}
+
+void CPureServerWhitelist::EncodeCommandList( CUtlDict<CPureServerWhitelist::CCommand*,int> &theList, CUtlBuffer &buf )
+{
+ // Count how many we're really going to write
+ int nCount = 0;
+ for ( int i=theList.First(); i != theList.InvalidIndex(); i = theList.Next( i ) )
+ {
+ if ( theList[i]->m_LoadOrder != kLoadOrder_HardcodedOverride )
+ ++nCount;
+ }
+ buf.PutInt( nCount );
+
+ // Write them
+ for ( int i=theList.First(); i != theList.InvalidIndex(); i = theList.Next( i ) )
+ {
+ CPureServerWhitelist::CCommand *pCommand = theList[i];
+ if ( pCommand->m_LoadOrder == kLoadOrder_HardcodedOverride )
+ continue;
+
+ unsigned char val = (unsigned char)pCommand->m_eFileClass;
+
+ buf.PutUnsignedChar( val );
+ buf.PutUnsignedShort( pCommand->m_LoadOrder );
+ buf.PutString( theList.GetElementName( i ) );
+ }
+}
+
+
+void CPureServerWhitelist::Decode( CUtlBuffer &buf )
+{
+ Term();
+
+ uint32 nVersionTag = *(uint32 *)buf.PeekGet();
+ uint32 nFormatVersion = 0;
+ if ( nVersionTag == 0xffff )
+ {
+ buf.GetUnsignedInt();
+ nFormatVersion = 1;
+ }
+ else
+ {
+ // Talking to legacy server -- load up default rules,
+ // the rest of his list are supposed to be exceptions to
+ // the base
+ Load( 2 );
+ }
+ DecodeCommandList( m_FileCommands, buf, nFormatVersion );
+ DecodeCommandList( m_RecursiveDirCommands, buf, nFormatVersion );
+ DecodeCommandList( m_NonRecursiveDirCommands, buf, nFormatVersion );
+
+ // Hardcoded
+ AddHardcodedFileCommands();
+
+ if ( nFormatVersion >= 1 )
+ {
+ uint32 nKeyCount = buf.GetUnsignedInt();
+ m_vecTrustedKeys.SetCount( nKeyCount );
+ FOR_EACH_VEC( m_vecTrustedKeys, i )
+ {
+ uint32 nKeySize = buf.GetUnsignedInt();
+ m_vecTrustedKeys[i].SetCount( nKeySize );
+ buf.Get( m_vecTrustedKeys[i].Base(), nKeySize );
+ }
+ }
+}
+
+
+void CPureServerWhitelist::CacheFileCRCs()
+{
+ InternalCacheFileCRCs( m_FileCommands, k_eCacheCRCType_SingleFile );
+ InternalCacheFileCRCs( m_NonRecursiveDirCommands, k_eCacheCRCType_Directory );
+ InternalCacheFileCRCs( m_RecursiveDirCommands, k_eCacheCRCType_Directory_Recursive );
+}
+
+
+// !SV_PURE FIXME! Do we need this?
+void CPureServerWhitelist::InternalCacheFileCRCs( CUtlDict<CCommand*,int> &theList, ECacheCRCType eType )
+{
+// for ( int i=theList.First(); i != theList.InvalidIndex(); i = theList.Next( i ) )
+// {
+// CCommand *pCommand = theList[i];
+// if ( pCommand->m_bCheckCRC )
+// {
+// const char *pPathname = theList.GetElementName( i );
+// m_pFileSystem->CacheFileCRCs( pPathname, eType, &m_ForceMatchList );
+// }
+// }
+}
+
+
+void CPureServerWhitelist::DecodeCommandList( CUtlDict<CPureServerWhitelist::CCommand*,int> &theList, CUtlBuffer &buf, uint32 nFormatVersion )
+{
+ int nCommands = buf.GetInt();
+
+ for ( int i=0; i < nCommands; i++ )
+ {
+ CPureServerWhitelist::CCommand *pCommand = new CPureServerWhitelist::CCommand;
+
+ unsigned char val = buf.GetUnsignedChar();
+ unsigned short nLoadOrder = buf.GetUnsignedShort();
+
+ if ( nFormatVersion == 0 )
+ {
+ pCommand->m_eFileClass = ( val & 1 ) ? ePureServerFileClass_Any : ePureServerFileClass_AnyTrusted;
+ pCommand->m_LoadOrder = nLoadOrder + m_LoadCounter;
+ }
+ else
+ {
+ pCommand->m_eFileClass = (EPureServerFileClass)val;
+ pCommand->m_LoadOrder = nLoadOrder;
+ }
+
+ char str[MAX_PATH];
+ buf.GetString( str );
+ V_FixSlashes( str );
+
+ theList.Insert( str, pCommand );
+ }
+}
+
+
+CPureServerWhitelist::CCommand* CPureServerWhitelist::GetBestEntry( const char *pFilename )
+{
+ // NOTE: Since this is a user-specified file, we don't have the added complexity of path IDs in here.
+ // So when the filesystem asks if a file is in the whitelist, we just ignore the path ID.
+
+ // Make sure we have a relative pathname with fixed slashes..
+ char relativeFilename[MAX_PATH];
+ V_strncpy( relativeFilename, pFilename, sizeof( relativeFilename ) );
+
+ // Convert the path to relative if necessary.
+ if ( !V_IsAbsolutePath( relativeFilename ) || m_pFileSystem->FullPathToRelativePath( pFilename, relativeFilename, sizeof( relativeFilename ) ) )
+ {
+ V_FixSlashes( relativeFilename );
+
+ // Get the directory this thing is in.
+ char relativeDir[MAX_PATH];
+ if ( !V_ExtractFilePath( relativeFilename, relativeDir, sizeof( relativeDir ) ) )
+ relativeDir[0] = 0;
+
+
+ // Check each of our dictionaries to see if there is an entry for this thing.
+ CCommand *pBestEntry = NULL;
+
+ pBestEntry = CheckEntry( m_FileCommands, relativeFilename, pBestEntry );
+ if ( relativeDir[0] != 0 )
+ {
+ pBestEntry = CheckEntry( m_NonRecursiveDirCommands, relativeDir, pBestEntry );
+
+ while ( relativeDir[0] != 0 )
+ {
+ // Check for this directory.
+ pBestEntry = CheckEntry( m_RecursiveDirCommands, relativeDir, pBestEntry );
+ if ( !V_StripLastDir( relativeDir, sizeof( relativeDir ) ) )
+ break;
+ }
+ }
+
+ return pBestEntry;
+ }
+
+ // Either we couldn't find an entry, or they specified an absolute path that we could not convert to a relative path.
+ return NULL;
+}
+
+
+CPureServerWhitelist::CCommand* CPureServerWhitelist::CheckEntry(
+ CUtlDict<CPureServerWhitelist::CCommand*,int> &dict,
+ const char *pEntryName,
+ CPureServerWhitelist::CCommand *pBestEntry )
+{
+ int i = dict.Find( pEntryName );
+ if ( i != dict.InvalidIndex() && (!pBestEntry || dict[i]->m_LoadOrder > pBestEntry->m_LoadOrder) )
+ pBestEntry = dict[i];
+
+ return pBestEntry;
+}
+
+
+void CPureServerWhitelist::AddRef()
+{
+ ThreadInterlockedIncrement( &m_RefCount );
+}
+
+void CPureServerWhitelist::Release()
+{
+ if ( ThreadInterlockedDecrement( &m_RefCount ) <= 0 )
+ delete this;
+}
+
+int CPureServerWhitelist::GetTrustedKeyCount() const
+{
+ return m_vecTrustedKeys.Count();
+}
+
+const byte *CPureServerWhitelist::GetTrustedKey( int iKeyIndex, int *nKeySize ) const
+{
+ Assert( nKeySize != NULL );
+ if ( !m_vecTrustedKeys.IsValidIndex( iKeyIndex ) )
+ {
+ *nKeySize = 0;
+ return NULL;
+ }
+
+ *nKeySize = m_vecTrustedKeys[iKeyIndex].Count();
+ return m_vecTrustedKeys[iKeyIndex].Base();
+}
+
+
+EPureServerFileClass CPureServerWhitelist::GetFileClass( const char *pszFilename )
+{
+ CCommand *pCommand = GetBestEntry( pszFilename );
+ if ( pCommand )
+ return pCommand->m_eFileClass;
+
+ // Default action is to be permissive. (The default whitelist protects certain directories and files at a root level.)
+ return ePureServerFileClass_Any;
+}
+
+void CPureFileTracker::AddUserReportedFileHash( int idxFile, FileHash_t *pFileHash, USERID_t userID, bool bAddMasterRecord )
+{
+ UserReportedFileHash_t userFileHash;
+ userFileHash.m_idxFile = idxFile;
+ userFileHash.m_userID = userID;
+ userFileHash.m_FileHash = *pFileHash;
+ int idxUserReported = m_treeUserReportedFileHash.Find( userFileHash );
+ if ( idxUserReported == m_treeUserReportedFileHash.InvalidIndex() )
+ {
+ idxUserReported = m_treeUserReportedFileHash.Insert( userFileHash );
+ if ( bAddMasterRecord )
+ {
+ // count the number of matches for this idxFile
+ // if it exceeds > 5 then make a master record
+ int idxFirst = idxUserReported;
+ int idxLast = idxUserReported;
+ int ctMatches = 1;
+ int ctTotalFiles = 1;
+ // first go forward
+ int idx = m_treeUserReportedFileHash.NextInorder( idxUserReported );
+ while ( idx != m_treeUserReportedFileHash.InvalidIndex() && m_treeUserReportedFileHash[idx].m_idxFile == m_treeUserReportedFileHash[idxUserReported].m_idxFile )
+ {
+ if ( m_treeUserReportedFileHash[idx].m_FileHash == m_treeUserReportedFileHash[idxUserReported].m_FileHash )
+ ctMatches++;
+ ctTotalFiles++;
+ idxLast = idx;
+ idx = m_treeUserReportedFileHash.NextInorder( idx );
+ }
+ // then backwards
+ idx = m_treeUserReportedFileHash.PrevInorder( idxUserReported );
+ while ( idx != m_treeUserReportedFileHash.InvalidIndex() && m_treeUserReportedFileHash[idx].m_idxFile == m_treeUserReportedFileHash[idxUserReported].m_idxFile )
+ {
+ if ( m_treeUserReportedFileHash[idx].m_FileHash == m_treeUserReportedFileHash[idxUserReported].m_FileHash )
+ ctMatches++;
+ ctTotalFiles++;
+ idxFirst = idx;
+ idx = m_treeUserReportedFileHash.PrevInorder( idx );
+ }
+ // if ctTotalFiles >> ctMatches then that means clients are reading different bits from the file.
+ // in order to get this right we need to ask them to read the entire thing
+ if ( ctMatches >= sv_pure_consensus.GetInt() )
+ {
+ MasterFileHash_t masterFileHashNew;
+ masterFileHashNew.m_idxFile = m_treeUserReportedFileHash[idxUserReported].m_idxFile;
+ masterFileHashNew.m_cMatches = ctMatches;
+ masterFileHashNew.m_FileHash = m_treeUserReportedFileHash[idxUserReported].m_FileHash;
+ m_treeMasterFileHashes.Insert( masterFileHashNew );
+ // remove all the individual records that matched the new master, we don't need them anymore
+ int idxRemove = idxFirst;
+ while ( idxRemove != m_treeUserReportedFileHash.InvalidIndex() )
+ {
+ int idxNext = m_treeUserReportedFileHash.NextInorder( idxRemove );
+ if ( m_treeUserReportedFileHash[idxRemove].m_FileHash == m_treeUserReportedFileHash[idxUserReported].m_FileHash )
+ m_treeUserReportedFileHash.RemoveAt( idxRemove );
+ if ( idxRemove == idxLast )
+ break;
+ idxRemove = idxNext;
+ }
+ }
+ }
+ }
+ else
+ {
+ m_treeUserReportedFileHash[idxUserReported].m_FileHash = *pFileHash;
+ }
+ // we dont have enough data to decide if you match or not yet - so we call it a match
+}
+
+
+void FileRenderHelper( USERID_t userID, const char *pchMessage, const char *pchPath, const char *pchFileName, FileHash_t *pFileHash, int nFileFraction, FileHash_t *pFileHashLocal )
+{
+ char rgch[256];
+ char hex[ 34 ];
+ Q_memset( hex, 0, sizeof( hex ) );
+ Q_binarytohex( (const byte *)&pFileHash->m_md5contents.bits, sizeof( pFileHash->m_md5contents.bits ), hex, sizeof( hex ) );
+
+ char hex2[ 34 ];
+ Q_memset( hex2, 0, sizeof( hex2 ) );
+ if ( pFileHashLocal )
+ Q_binarytohex( (const byte *)&pFileHashLocal->m_md5contents.bits, sizeof( pFileHashLocal->m_md5contents.bits ), hex2, sizeof( hex2 ) );
+
+ if ( pFileHash->m_PackFileID )
+ {
+ Q_snprintf( rgch, 256, "Pure server: file: %s\\%s ( %d %d %8.8x %6.6x ) %s : %s : %s\n",
+ pchPath, pchFileName,
+ pFileHash->m_PackFileID, pFileHash->m_nPackFileNumber, nFileFraction, pFileHash->m_cbFileLen,
+ pchMessage,
+ hex, hex2 );
+ }
+ else
+ {
+ Q_snprintf( rgch, 256, "Pure server: file: %s\\%s ( %d %d %x ) %s : %s : %s\n",
+ pchPath, pchFileName,
+ pFileHash->m_eFileHashType, pFileHash->m_cbFileLen, pFileHash->m_eFileHashType ? pFileHash->m_crcIOSequence : 0,
+ pchMessage,
+ hex, hex2 );
+ }
+ if ( userID.idtype != 0 )
+ Msg( "[%s] %s\n", GetUserIDString(userID), rgch );
+ else
+ Msg( "%s", rgch );
+
+}
+
+
+bool CPureFileTracker::DoesFileMatch( const char *pPathID, const char *pRelativeFilename, int nFileFraction, FileHash_t *pFileHash, USERID_t userID )
+{
+// // if the server has been idle for more than 15 minutes, discard all this data
+// const float flRetireTime = sv_pure_retiretime.GetFloat();
+// float flCurTime = Plat_FloatTime();
+// if ( ( flCurTime - m_flLastFileReceivedTime ) > flRetireTime )
+// {
+// m_treeMasterFileHashes.RemoveAll();
+// m_treeUserReportedFileHash.RemoveAll();
+// m_treeMasterFileHashes.RemoveAll();
+// }
+// m_flLastFileReceivedTime = flCurTime;
+//
+// // The clients must send us all files. We decide if it is whitelisted or not
+// // That way the clients can not hide modified files in a whitelisted directory
+// if ( pFileHash->m_PackFileID == 0 &&
+// sv.GetPureServerWhitelist()->GetFileClass( pRelativeFilename ) != ePureServerFileClass_CheckHash )
+// {
+//
+// if ( sv_pure_trace.GetInt() == 4 )
+// {
+// char warningStr[1024] = {0};
+// V_snprintf( warningStr, sizeof( warningStr ), "Pure server: file [%s]\\%s ignored by whitelist.", pPathID, pRelativeFilename );
+// Msg( "[%s] %s\n", GetUserIDString(userID), warningStr );
+// }
+//
+// return true;
+// }
+//
+// char rgchFilenameFixed[MAX_PATH];
+// Q_strncpy( rgchFilenameFixed, pRelativeFilename, sizeof( rgchFilenameFixed ) );
+// Q_FixSlashes( rgchFilenameFixed );
+//
+// // first look up the file and see if we have ever seen it before
+// CRC32_t crcFilename;
+// CRC32_Init( &crcFilename );
+// CRC32_ProcessBuffer( &crcFilename, rgchFilenameFixed, Q_strlen( rgchFilenameFixed ) );
+// CRC32_ProcessBuffer( &crcFilename, pPathID, Q_strlen( pPathID ) );
+// CRC32_Final( &crcFilename );
+// UserReportedFile_t ufile;
+// ufile.m_crcIdentifier = crcFilename;
+// ufile.m_filename = rgchFilenameFixed;
+// ufile.m_path = pPathID;
+// ufile.m_nFileFraction = nFileFraction;
+// int idxFile = m_treeAllReportedFiles.Find( ufile );
+// if ( idxFile == m_treeAllReportedFiles.InvalidIndex() )
+// {
+// idxFile = m_treeAllReportedFiles.Insert( ufile );
+// }
+// else
+// {
+// m_cMatchedFile++;
+// }
+// // then check if we have a master CRC for the file
+// MasterFileHash_t masterFileHash;
+// masterFileHash.m_idxFile = idxFile;
+// int idxMaster = m_treeMasterFileHashes.Find( masterFileHash );
+// // dont do anything with this yet
+//
+// // check to see if we have loaded the file locally and can match it
+// FileHash_t filehashLocal;
+// EFileCRCStatus eStatus = g_pFileSystem->CheckCachedFileHash( pPathID, rgchFilenameFixed, nFileFraction, &filehashLocal );
+// if ( eStatus == k_eFileCRCStatus_FileInVPK)
+// {
+// // you managed to load a file outside a VPK that the server has in the VPK
+// // this is possible if the user explodes the VPKs into individual files and then deletes the VPKs
+// FileRenderHelper( userID, "file should be in VPK", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, NULL );
+// return false;
+// }
+// // if the user sent us a full file hash, but we dont have one, hash it now
+// if ( pFileHash->m_eFileHashType == FileHash_t::k_EFileHashTypeEntireFile &&
+// ( eStatus != k_eFileCRCStatus_GotCRC || filehashLocal.m_eFileHashType != FileHash_t::k_EFileHashTypeEntireFile ) )
+// {
+// // lets actually read the file so we get a complete file hash
+// FileHandle_t f = g_pFileSystem->Open( rgchFilenameFixed, "rb", pPathID);
+// // try to load the file and really compute the hash - should only have to do this once ever
+// if ( f )
+// {
+// // load file into a null-terminated buffer
+// int fileSize = g_pFileSystem->Size( f );
+// unsigned bufSize = g_pFileSystem->GetOptimalReadSize( f, fileSize );
+//
+// char *buffer = (char*)g_pFileSystem->AllocOptimalReadBuffer( f, bufSize );
+// Assert( buffer );
+//
+// // read into local buffer
+// bool bRetOK = ( g_pFileSystem->ReadEx( buffer, bufSize, fileSize, f ) != 0 );
+// bRetOK;
+// g_pFileSystem->FreeOptimalReadBuffer( buffer );
+//
+// g_pFileSystem->Close( f ); // close file after reading
+//
+// eStatus = g_pFileSystem->CheckCachedFileHash( pPathID, rgchFilenameFixed, nFileFraction, &filehashLocal );
+// }
+// else
+// {
+// // what should we do if we couldn't open the file? should probably kick
+// FileRenderHelper( userID, "could not open file to hash ( benign for now )", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, NULL );
+// }
+// }
+// if ( eStatus == k_eFileCRCStatus_GotCRC )
+// {
+// if ( filehashLocal.m_eFileHashType == FileHash_t::k_EFileHashTypeEntireFile &&
+// pFileHash->m_eFileHashType == FileHash_t::k_EFileHashTypeEntireFile )
+// {
+// if ( filehashLocal == *pFileHash )
+// {
+// m_cMatchedFileFullHash++;
+// return true;
+// }
+// else
+// {
+// // don't need to check anything else
+// // did not match - record so that we have a record of the file that did not match ( just for reporting )
+// AddUserReportedFileHash( idxFile, pFileHash, userID, false );
+// FileRenderHelper( userID, "file does not match", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, &filehashLocal );
+// return false;
+// }
+// }
+// }
+//
+// // if this is a VPK file, we have completely cataloged all the VPK files, so no suprises are allowed
+// if ( pFileHash->m_PackFileID )
+// {
+// AddUserReportedFileHash( idxFile, pFileHash, userID, false );
+// FileRenderHelper( userID, "unrecognized vpk file", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, NULL );
+// return false;
+// }
+//
+//
+// // now lets see if we have a master file hash for this
+// if ( idxMaster != m_treeMasterFileHashes.InvalidIndex() )
+// {
+// m_cMatchedMasterFile++;
+//
+// FileHash_t *pFileHashLocal = &m_treeMasterFileHashes[idxMaster].m_FileHash;
+// if ( *pFileHashLocal == *pFileHash )
+// {
+// m_cMatchedMasterFileHash++;
+// return true;
+// }
+// else
+// {
+// // did not match - record so that we have a record of the file that did not match ( just for reporting )
+// AddUserReportedFileHash( idxFile, pFileHash, userID, false );
+// // and then return failure
+// FileRenderHelper( userID, "file does not match server master file", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, pFileHashLocal );
+// return false;
+// }
+// }
+//
+// // no master record, accumulate individual record so we can get a consensus
+// if ( sv_pure_trace.GetInt() == 3 )
+// {
+// FileRenderHelper( userID, "server does not have hash for this file. Waiting for consensus", pPathID, rgchFilenameFixed, pFileHash, nFileFraction, NULL );
+// }
+//
+// AddUserReportedFileHash( idxFile, pFileHash, userID, true );
+
+ // we dont have enough data to decide if you match or not yet - so we call it a match
+ return true;
+}
+
+
+struct FindFileIndex_t
+{
+ int idxFindFile;
+};
+
+class CStupidLess
+{
+public:
+ bool Less( const FindFileIndex_t &src1, const FindFileIndex_t &src2, void *pCtx )
+ {
+ if ( src1.idxFindFile < src2.idxFindFile )
+ return true;
+
+ return false;
+ }
+};
+
+int CPureFileTracker::ListUserFiles( bool bListAll, const char *pchFilenameFind )
+{
+ CUtlSortVector< FindFileIndex_t, CStupidLess > m_vecReportedFiles;
+ int idxFindFile = m_treeAllReportedFiles.FirstInorder();
+ while ( idxFindFile != m_treeAllReportedFiles.InvalidIndex() )
+ {
+ UserReportedFile_t &ufile = m_treeAllReportedFiles[idxFindFile];
+ if ( pchFilenameFind && Q_stristr( ufile.m_filename, pchFilenameFind ) )
+ {
+ FileHash_t filehashLocal;
+ EFileCRCStatus eStatus = g_pFileSystem->CheckCachedFileHash( ufile.m_path.String(), ufile.m_filename.String(), 0, &filehashLocal );
+
+ if ( eStatus == k_eFileCRCStatus_GotCRC )
+ {
+ USERID_t useridFake;
+ useridFake.idtype = IDTYPE_STEAM;
+ FileRenderHelper( useridFake, "Found: ",ufile.m_path.String(),ufile.m_filename.String(), &filehashLocal, 0, NULL );
+ FindFileIndex_t ffi;
+ ffi.idxFindFile = idxFindFile;
+ m_vecReportedFiles.Insert( ffi );
+ }
+ else
+ {
+ Msg( "File not found %s %s %x\n", ufile.m_filename.String(), ufile.m_path.String(), idxFindFile );
+ }
+ }
+ idxFindFile = m_treeAllReportedFiles.NextInorder( idxFindFile );
+ }
+
+
+ int cTotalFiles = 0;
+ int cTotalMatches = 0;
+ int idx = m_treeUserReportedFileHash.FirstInorder();
+ while ( idx != m_treeUserReportedFileHash.InvalidIndex() )
+ {
+ UserReportedFileHash_t &file = m_treeUserReportedFileHash[idx];
+
+ int idxNext = m_treeUserReportedFileHash.NextInorder( idx );
+ int ctMatches = 1;
+ int ctFiles = 1;
+ // check this against all others for the same file
+ while ( idxNext != m_treeUserReportedFileHash.InvalidIndex() && m_treeUserReportedFileHash[idx].m_idxFile == m_treeUserReportedFileHash[idxNext].m_idxFile )
+ {
+ if ( m_treeUserReportedFileHash[idx].m_FileHash == m_treeUserReportedFileHash[idxNext].m_FileHash )
+ {
+ ctMatches++;
+ cTotalMatches++;
+ }
+ ctFiles++;
+ idxNext = m_treeUserReportedFileHash.NextInorder( idxNext );
+ }
+ idx = m_treeUserReportedFileHash.NextInorder( idx );
+ cTotalFiles++;
+
+ // do we have a master for this one?
+ MasterFileHash_t masterFileHashFind;
+ masterFileHashFind.m_idxFile = file.m_idxFile;
+ int idxMaster = m_treeMasterFileHashes.Find( masterFileHashFind );
+
+ UserReportedFile_t &ufile = m_treeAllReportedFiles[file.m_idxFile];
+
+ bool bOutput = false;
+ if ( Q_stristr( ufile.m_filename.String(), "bin\\pak01" )!=NULL || Q_stristr( ufile.m_filename.String(), ".vpk" )!=NULL )
+ bOutput = true;
+ else
+ {
+ FileHash_t filehashLocal;
+ EFileCRCStatus eStatus = g_pFileSystem->CheckCachedFileHash( ufile.m_path.String(), ufile.m_filename.String(), 0, &filehashLocal );
+ if ( eStatus == k_eFileCRCStatus_GotCRC )
+ {
+ if ( filehashLocal.m_eFileHashType == FileHash_t::k_EFileHashTypeEntireFile &&
+ file.m_FileHash.m_eFileHashType == FileHash_t::k_EFileHashTypeEntireFile &&
+ filehashLocal != file.m_FileHash )
+ {
+ bOutput = true;
+ }
+ }
+ }
+
+ FindFileIndex_t ffi;
+ ffi.idxFindFile = file.m_idxFile;
+
+ if ( ctMatches != ctFiles || idxMaster != m_treeMasterFileHashes.InvalidIndex() || bListAll || ( pchFilenameFind && m_vecReportedFiles.Find( ffi ) != -1 ) || bOutput )
+ {
+ char rgch[256];
+ Q_snprintf( rgch, 256, "reports=%d matches=%d Hash details:", ctFiles, ctMatches );
+ FileHash_t *pFileHashMaster = NULL;
+ if ( idxMaster != m_treeMasterFileHashes.InvalidIndex() )
+ pFileHashMaster = &m_treeMasterFileHashes[idxMaster].m_FileHash;
+ FileRenderHelper( file.m_userID, rgch, ufile.m_path.String(), ufile.m_filename.String(), &file.m_FileHash, 0, pFileHashMaster );
+ }
+ }
+ Msg( "Total user files %d %d %d \n", m_treeUserReportedFileHash.Count(), cTotalFiles, cTotalMatches );
+ Msg( "Total files %d, total with authoritative hashes %d \n", m_treeAllReportedFiles.Count(), m_treeMasterFileHashes.Count() );
+ Msg( "Matching files %d %d %d \n", m_cMatchedFile, m_cMatchedMasterFile, m_cMatchedMasterFileHash );
+
+ return 0;
+}
+
+int CPureFileTracker::ListAllTrackedFiles( bool bListAll, const char *pchFilenameFind, int nFileFractionMin, int nFileFractionMax )
+{
+ g_pFileSystem->MarkAllCRCsUnverified();
+
+ int cTotal = 0;
+ int cTotalMatch = 0;
+ int count = 0;
+ do
+ {
+ CUnverifiedFileHash rgUnverifiedFiles[1];
+ count = g_pFileSystem->GetUnverifiedFileHashes( rgUnverifiedFiles, ARRAYSIZE( rgUnverifiedFiles ) );
+
+ if ( count && ( bListAll || ( pchFilenameFind && Q_stristr( rgUnverifiedFiles[0].m_Filename, pchFilenameFind ) && rgUnverifiedFiles[0].m_nFileFraction >= nFileFractionMin && rgUnverifiedFiles[0].m_nFileFraction <= nFileFractionMax ) ) )
+ {
+ USERID_t useridFake;
+ useridFake.idtype = IDTYPE_STEAM;
+ FileRenderHelper( useridFake, "", rgUnverifiedFiles[0].m_PathID, rgUnverifiedFiles[0].m_Filename, &rgUnverifiedFiles[0].m_FileHash, rgUnverifiedFiles[0].m_nFileFraction, NULL );
+ if ( rgUnverifiedFiles[0].m_FileHash.m_PackFileID )
+ {
+ g_pFileSystem->CheckVPKFileHash( rgUnverifiedFiles[0].m_FileHash.m_PackFileID, rgUnverifiedFiles[0].m_FileHash.m_nPackFileNumber, rgUnverifiedFiles[0].m_nFileFraction, rgUnverifiedFiles[0].m_FileHash.m_md5contents );
+ }
+ cTotalMatch++;
+ }
+ if ( count )
+ cTotal++;
+ } while ( count );
+
+ Msg( "Total files %d Matching files %d \n", cTotal, cTotalMatch );
+
+ return 0;
+}
+
+
+CPureFileTracker g_PureFileTracker;
+
+//#define DEBUG_PURE_SERVER
+#ifdef DEBUG_PURE_SERVER
+void CC_ListPureServerFiles(const CCommand &args)
+{
+ if ( !sv.IsDedicated() )
+ return;
+ g_PureFileTracker.ListUserFiles( args.ArgC() > 1 && (atoi(args[1]) > 0), NULL );
+}
+
+static ConCommand svpurelistuserfiles("sv_pure_listuserfiles", CC_ListPureServerFiles, "ListPureServerFiles");
+
+
+void CC_PureServerFindFile(const CCommand &args)
+{
+ if ( !sv.IsDedicated() )
+ return;
+ g_PureFileTracker.ListUserFiles( false, args[1] );
+}
+
+static ConCommand svpurefinduserfiles("sv_pure_finduserfiles", CC_PureServerFindFile, "ListPureServerFiles");
+
+void CC_PureServerListTrackedFiles(const CCommand &args)
+{
+ // BUGBUG! Because this code is in engine instead of server, it exists in the client - ugh!
+ // Remove this command from client before shipping for realz.
+ //if ( !sv.IsDedicated() )
+ // return;
+ int nFileFractionMin = args.ArgC() >= 3 ? Q_atoi(args[2]) : 0;
+ int nFileFractionMax = args.ArgC() >= 4 ? Q_atoi(args[3]) : nFileFractionMin;
+ if ( nFileFractionMax < 0 )
+ nFileFractionMax = 0x7FFFFFFF;
+ g_PureFileTracker.ListAllTrackedFiles( args.ArgC() <= 1, args.ArgC() >= 2 ? args[1] : NULL, nFileFractionMin, nFileFractionMax );
+}
+
+static ConCommand svpurelistfiles("sv_pure_listfiles", CC_PureServerListTrackedFiles, "ListPureServerFiles");
+
+void CC_PureServerCheckVPKFiles(const CCommand &args)
+{
+ if ( sv.IsDedicated() )
+ Plat_BeginWatchdogTimer( 5 * 60 ); // reset watchdog timer to allow 5 minutes for the VPK check
+ g_pFileSystem->CacheAllVPKFileHashes( false, true );
+ if ( sv.IsDedicated() )
+ Plat_EndWatchdogTimer();
+}
+
+static ConCommand svpurecheckvpks("sv_pure_checkvpk", CC_PureServerCheckVPKFiles, "CheckPureServerVPKFiles");
+
+#endif