diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /engine/pure_server.cpp | |
| download | archived-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.cpp | 1145 |
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 |