diff options
Diffstat (limited to 'gcsdk/gcsession.cpp')
| -rw-r--r-- | gcsdk/gcsession.cpp | 607 |
1 files changed, 607 insertions, 0 deletions
diff --git a/gcsdk/gcsession.cpp b/gcsdk/gcsession.cpp new file mode 100644 index 0000000..b739398 --- /dev/null +++ b/gcsdk/gcsession.cpp @@ -0,0 +1,607 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Holds the CGCSession class +// +//============================================================================= +#include "stdafx.h" +#include "gcsession.h" +#include "steamextra/rtime.h" +#include "gcsdk_gcmessages.pb.h" +#include "gcsdk/gcreportprinter.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// Probably this makes more sense true by default, but we're spewing a ton and Fletcher says it +// isn't a big deal for TF so here we go. +GCConVar gs_session_assert_valid_addr_and_port( "gs_session_assert_valid_addr_and_port", "0" ); + +namespace GCSDK +{ + +DECLARE_GC_EMIT_GROUP( g_EGSessions, sessions ); +DECLARE_GC_EMIT_GROUP_DEFAULTS( g_EGRateLimit, ratelimit, 2, 3 ); + +GCConVar max_user_messages_per_second( "max_user_messages_per_second", "20", 0, "Maximum number of messages a user can send per second. 0 disables the rate limiting" ); +static GCConVar user_message_rate_limit_warning_period( "user_message_rate_limit_warning_period", "30", 0, "Number of seconds between warning about rate limiting for users" ); + +static GCConVar msg_rate_limit_report_user_bucket_1( "msg_rate_limit_report_user_bucket_1", "10", 0, "These values control where various users are bucketed in rate limiting reports to help identify how frequently users are running into rate limiting" ); +static GCConVar msg_rate_limit_report_user_bucket_2( "msg_rate_limit_report_user_bucket_2", "100", 0, "These values control where various users are bucketed in rate limiting reports to help identify how frequently users are running into rate limiting" ); + +static GCConVar msg_rate_limit_list_user( "msg_rate_limit_list_user", "0", 0, "When set to a user account ID, this will report all the messages that are rate limited for that user to the console" ); + +CMsgRateLimitTracker g_RateLimitTracker; + + +CMsgRateLimitTracker::CMsgRateLimitTracker() : + m_StartTime( CRTime::RTime32TimeCur() ) +{ +} + +void CMsgRateLimitTracker::TrackRateLimitedMsg( const CSteamID steamID, MsgType_t eMsgType ) +{ + //update message stat + { + uint32 nMsgIndex = m_MsgStats.Find( eMsgType ); + if( !m_MsgStats.IsValidIndex( nMsgIndex ) ) + { + nMsgIndex = m_MsgStats.Insert( eMsgType, 0 ); + } + m_MsgStats[ nMsgIndex ]++; + } + + //update user stats + { + uint32 nUserIndex = m_UserStats.Find( steamID ); + if( !m_UserStats.IsValidIndex( nUserIndex ) ) + { + nUserIndex = m_UserStats.Insert( steamID, 0 ); + } + m_UserStats[ nUserIndex ]++; + } + + //determine the severity to output the warning at. Assume verbose unless we are tracking a specific account ID (note that no account has 0 so 0 still effectively turns it off) + CGCEmitGroup::EMsgLevel eMsgLevel = CGCEmitGroup::kMsg_Verbose; + if( ( uint32 )msg_rate_limit_list_user.GetInt() == steamID.GetAccountID() ) + { + eMsgLevel = CGCEmitGroup::kMsg_Msg; + } + EG_EMIT( g_EGMessages, eMsgLevel, "Dropped message %s (%d) for user %s\n", PchMsgNameFromEMsg( eMsgType ), eMsgType, steamID.Render() ); +} + +void CMsgRateLimitTracker::ReportMsgStats() const +{ + CGCReportPrinter rp; + rp.AddStringColumn( "Msg" ); + rp.AddIntColumn( "Count", CGCReportPrinter::eSummary_Total ); + + FOR_EACH_MAP_FAST( m_MsgStats, nCurrMsg ) + { + rp.StrValue( PchMsgNameFromEMsg( m_MsgStats.Key( nCurrMsg ) ) ); + rp.IntValue( m_MsgStats[ nCurrMsg ] ); + rp.CommitRow(); + } + + rp.SortReport( "Count" ); + rp.PrintReport( SPEW_CONSOLE ); +} + +void CMsgRateLimitTracker::ReportTopUsers( uint32 nMinMsgs, uint32 nListTop ) const +{ + //collect a list of all messages, and sort them into order of frequency + CGCReportPrinter rp; + rp.AddSteamIDColumn( "User" ); + rp.AddIntColumn( "Count", CGCReportPrinter::eSummary_Total ); + + FOR_EACH_MAP_FAST( m_UserStats, nCurrMsg ) + { + rp.SteamIDValue( m_UserStats.Key( nCurrMsg ) ); + rp.IntValue( m_UserStats[ nCurrMsg ] ); + rp.CommitRow(); + } + + rp.SortReport( "Count" ); + rp.PrintReport( SPEW_CONSOLE, nListTop ); +} + +void CMsgRateLimitTracker::ReportUserStats() const +{ + //run through the users and aggregate stats + const uint32 nBucketLimit1 = ( uint32 )max( 0, min( msg_rate_limit_report_user_bucket_1.GetInt(), msg_rate_limit_report_user_bucket_2.GetInt() ) ); + const uint32 nBucketLimit2 = ( uint32 )max( 0, max( msg_rate_limit_report_user_bucket_1.GetInt(), msg_rate_limit_report_user_bucket_2.GetInt() ) ); + + uint32 nTotalMsg = 0; + uint32 nMaxUser = 0; + uint32 nBucketCount1 = 0; + uint32 nBucketCount2 = 0; + FOR_EACH_MAP_FAST( m_UserStats, nCurrMsg ) + { + //add user counts to the buckets + const uint32 nMsgs = m_UserStats[ nCurrMsg ]; + if( nMsgs <= nBucketLimit1 ) + nBucketCount1++; + else if( nMsgs <= nBucketLimit2 ) + nBucketCount2++; + + //add up our total number of offenses + nTotalMsg += nMsgs; + nMaxUser = max( nMaxUser, nMsgs ); + } + + EG_MSG( SPEW_CONSOLE, "Capture Duration: %ds\n", CRTime::RTime32TimeCur() - m_StartTime ); + EG_MSG( SPEW_CONSOLE, "Total Dropped Messages: %d\n", nTotalMsg ); + EG_MSG( SPEW_CONSOLE, "Message IDs: %d\n", m_MsgStats.Count() ); + EG_MSG( SPEW_CONSOLE, "Users: %d (peak: %d)\n", m_UserStats.Count(), nMaxUser ); + EG_MSG( SPEW_CONSOLE, " Below %d msgs: %d\n", nBucketLimit1, nBucketCount1 ); + EG_MSG( SPEW_CONSOLE, " Below %d msgs: %d\n", nBucketLimit2, nBucketCount2 ); +} + +void CMsgRateLimitTracker::ClearStats() +{ + m_StartTime = CRTime::RTime32TimeCur(); + m_UserStats.RemoveAll(); + m_MsgStats.RemoveAll(); +} + +//console command hooks +GC_CON_COMMAND( msg_rate_limit_dump, "Dumps stats about rate limiting of messages" ) +{ + g_RateLimitTracker.ReportUserStats(); + g_RateLimitTracker.ReportMsgStats(); + g_RateLimitTracker.ReportTopUsers( 0, 20 ); +} + +GC_CON_COMMAND( msg_rate_limit_dump_users, "Dumps a list of users that have been rate limited. Optional parameters can specify the number to dump or the minimum number of messages required." ) +{ + if( args.ArgC() < 3 ) + { + EG_MSG( SPEW_CONSOLE, "Proper usage is: %s <min messages> <top users> - Specify 0 for one or both to have it be ignored\n", args[ 0 ] ); + return; + } + g_RateLimitTracker.ReportTopUsers( ( uint32 )max( 0, atoi( args[ 1 ] ) ), ( uint32 )max( 0, atoi( args[ 2 ] ) ) ); +} + +GC_CON_COMMAND( msg_rate_limit_dump_msgs, "Dumps a list of messages that have been rate limited." ) +{ + g_RateLimitTracker.ReportMsgStats(); +} + +GC_CON_COMMAND( msg_rate_limit_clear, "Clears all the accumulated msg rate limit stats" ) +{ + g_RateLimitTracker.ClearStats(); +} + +//------------------------------------------------------------------------------------------ +// CSteamIDRateLimit +//------------------------------------------------------------------------------------------ + +CSteamIDRateLimit::CSteamIDRateLimit( const GCConVar& cvNumPerPeriod, const GCConVar* pcvPeriodS ) : + m_cvNumPerPeriod( cvNumPerPeriod ), + m_pcvPeriodS( pcvPeriodS ), + m_LastClear( CRTime::RTime32TimeCur() ), + m_FrameFunction( "SteamIDRateLimit", CBaseFrameFunction::k_EFrameType_RunOnce ) +{ + m_FrameFunction.Register( this, &CSteamIDRateLimit::OnFrameFn ); +} + +CSteamIDRateLimit::~CSteamIDRateLimit() +{ +} + +bool CSteamIDRateLimit::BIsRateLimited( CSteamID steamID, uint32 unMsgType ) +{ + int nIndex = m_Msgs.FindOrInsert( steamID, 0 ); + if( ++m_Msgs[ nIndex ] >= ( uint32 )m_cvNumPerPeriod.GetInt() ) + { + g_RateLimitTracker.TrackRateLimitedMsg( steamID, unMsgType ); + return true; + } + return false; +} + +bool CSteamIDRateLimit::OnFrameFn( const CLimitTimer& timer ) +{ + //if no period is specified, assume one second + int nIntervalS = ( m_pcvPeriodS ) ? MAX( 1, m_pcvPeriodS->GetInt() ) : 1; + if( CRTime::RTime32TimeCur() >= m_LastClear + nIntervalS ) + { + m_Msgs.RemoveAll(); + m_LastClear = CRTime::RTime32TimeCur(); + } + return false; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CGCSession::CGCSession( const CSteamID & steamID, CGCSharedObjectCache *pSOCache ) +: m_steamID( steamID ), + m_pSOCache( pSOCache ), + m_bIsShuttingDown( false ), + m_osType( k_eOSUnknown ), + m_bIsTestSession( false ), + m_bIsSecure( false ), + m_unIPPublic( 0 ), + m_flLatitude( 0.0f ), + m_flLongitude( 0.0f ) , + m_haveGeoLocation( false ), + m_bInitialized( false ), + m_rtLastMessageReceived( 0 ) +{ + m_jtLastMessageReceived.SetLTime( 0 ); + m_jtTimeSentPing.SetLTime( 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CGCSession::~CGCSession() +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Checks the message against rate limiting. Returns true if we should +// drop the message. False otherwise. This default behavior is a very basic +// n messages / user / second rate limiting that's only meant to stop the +// worse abuses +//----------------------------------------------------------------------------- +bool CGCSession::BRateLimitMessage( MsgType_t unMsgType ) +{ + unMsgType &= ~k_EMsgProtoBufFlag; + if ( max_user_messages_per_second.GetInt() <= 0 ) + return false; + + RTime32 rtCur = CRTime::RTime32TimeCur(); + m_jtLastMessageReceived.SetToJobTime(); + if ( m_rtLastMessageReceived != rtCur ) + { + m_rtLastMessageReceived = rtCur; + m_unMessagesRecievedThisSecond = 0; + } + + m_unMessagesRecievedThisSecond++; + if ( m_unMessagesRecievedThisSecond > (uint32)max_user_messages_per_second.GetInt() ) + { + //log this message + g_RateLimitTracker.TrackRateLimitedMsg( GetSteamID(), unMsgType ); + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: The run function is called on each session (user and gameserver) +// approximately every k_nUserSessionRunInterval microseconds (or +// k_nGSSessionRunInterval for GS Sessions) +//----------------------------------------------------------------------------- +void CGCSession::Run() +{ + // These cached subscription messages are very expensive and only needed for a short period of time + // If we're hitting the run loop, it's been around long enough + GetSOCache()->ClearCachedSubscriptionMessage(); +} + + +//----------------------------------------------------------------------------- +bool CGCSession::GetGeoLocation( float &latitude, float &longittude ) const +{ + latitude = m_flLatitude; + longittude = m_flLongitude; + + return m_haveGeoLocation; +} + +//----------------------------------------------------------------------------- +void CGCSession::SetGeoLocation( float latitude, float longittude ) +{ + m_flLatitude = latitude; + m_flLongitude = longittude; + m_haveGeoLocation = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Claims all the memory for the session object +//----------------------------------------------------------------------------- +#ifdef DBGFLAG_VALIDATE +void CGCSession::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); +} +#endif // DBGFLAG_VALIDATE + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CGCUserSession::~CGCUserSession() +{ + if ( m_steamIDGS.BGameServerAccount() ) + { + EmitError( SPEW_GC, "Destroying user %s while still connected to server %s\n", GetSteamID().Render(), GetSteamIDGS().Render() ); + } +} + +bool CGCUserSession::BInit() +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the session's game server to the given SteamID. This will +// cause the session to leave the current server it's on, it any +// Returns: True if the user's session was added to the GS's session. +// False if the session could not be found or if the user was already +// on the server. +//----------------------------------------------------------------------------- +bool CGCUserSession::BSetServer( const CSteamID &steamIDGS ) +{ + if ( steamIDGS == m_steamIDGS ) + return false; + + BLeaveServer(); + + if( steamIDGS.IsValid() ) + { + CGCGSSession *pGSSession = GGCBase()->FindGSSession( steamIDGS ); + if ( !pGSSession ) + { + EmitError( SPEW_GC, "User %s attempting to join server %s which has no session\n", GetSteamID().Render(), steamIDGS.Render() ); + return false; + } + + if ( !pGSSession->BAddUser( GetSteamID() ) ) + { + EmitWarning( SPEW_GC, SPEW_ALWAYS, "Server %s already had user %s in its user list\n", steamIDGS.Render(), GetSteamID().Render() ); + // Fall through + } + } + + m_steamIDGS = steamIDGS; + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Removes the session from the given game server +// Returns: True if the user's session was removed from the GS's session. +// False if the session could not be found or if the user was not found +// on the server. +//----------------------------------------------------------------------------- +bool CGCUserSession::BLeaveServer() +{ + if( m_steamIDGS.IsValid() ) + { + // Remember the last server we were connected to + m_steamIDGSPrev = m_steamIDGS; + + CGCGSSession *pGSSession = GGCBase()->FindGSSession( m_steamIDGS ); + if ( pGSSession ) + { + pGSSession->BRemoveUser( GetSteamID() ); + } + } + + m_steamIDGS = CSteamID(); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Dumps useful information about this session +//----------------------------------------------------------------------------- +void CGCUserSession::Dump( bool bFull ) const +{ +// this is ifdef'd out in Steam because GCSDK can't depend on steamid.cpp +#ifndef STEAM + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "User Session %s (%s)\n", GetSteamID().Render(), BIsShuttingDown() ? "SHUTTING DOWN" : "Active" ); + CJob *pJob = GGCBase()->PJobHoldingLock( GetSteamID() ); + if( pJob ) + { + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\t LOCKED BY: %s\n", pJob->GetName() ); + } + if( bFull && GetSOCache() ) + GetSOCache()->Dump(); + if( GetSteamIDGS().BGameServerAccount() ) + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tGameserver: %s\n", GetSteamIDGS().Render() ); + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tOS: %d Secure: %d\n", GetOSType(), IsSecure() ? 1 : 0 ); +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CGCGSSession::CGCGSSession( const CSteamID & steamID, CGCSharedObjectCache *pCache, uint32 unServerAddr, uint16 usServerPort ) +: CGCSession( steamID, pCache ), m_unServerAddr( unServerAddr ), m_usServerPort( usServerPort ) +{ + if ( gs_session_assert_valid_addr_and_port.GetBool() ) + { + Assert( unServerAddr ); + Assert( usServerPort ); + } + + // Default our public IP to be the same as our IP address + m_unIPPublic = unServerAddr; +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CGCGSSession::~CGCGSSession() +{ + if ( m_vecUsers.Count() > 0 ) + { + EmitError( SPEW_GC, "Destroying game server %s while %d users are still connected\n", GetSteamID().Render(), m_vecUsers.Count() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a user to the list of users active on the game server +// Returns: True if the user was added, false if the user was already on this +// server. +//----------------------------------------------------------------------------- +bool CGCGSSession::BAddUser( const CSteamID &steamIDUser ) +{ + if( m_vecUsers.HasElement( steamIDUser ) ) + return false; + + PreAddUser( steamIDUser ); + m_vecUsers.AddToTail( steamIDUser ); + PostAddUser( steamIDUser ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Called if our IP / port changes after session is started +//----------------------------------------------------------------------------- +void CGCGSSession::SetIPAndPort( uint32 unServerAddr, uint16 usServerPort ) +{ + // If we didn't have an override for the public IP, then also + // update the public IP. + // + // !KLUDGE! This is gross for two reasons: + // - First, do we really need two different fields? + // - Second, why can the IP change after the session is created? + // Shouldn't we force the session to be destroyed and recreated? + // It cannot *really* be the same "session", can it? + if ( m_unIPPublic == m_unServerAddr ) + m_unIPPublic = unServerAddr; + m_unServerAddr = unServerAddr; + m_usServerPort = usServerPort; +} + +//----------------------------------------------------------------------------- +// Purpose: Removes a user from the list of users active on the game server +// Returns: True if the user was added, false if the user was not already on +// this server. +//----------------------------------------------------------------------------- +bool CGCGSSession::BRemoveUser( const CSteamID &steamIDUser ) +{ + int nIndex = m_vecUsers.Find( steamIDUser ); + if ( !m_vecUsers.IsValidIndex( nIndex ) ) + return false; + + PreRemoveUser( steamIDUser ); + m_vecUsers.Remove( nIndex ); + PostRemoveUser( steamIDUser ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Removes all users from the list of game server users. +//----------------------------------------------------------------------------- +void CGCGSSession::RemoveAllUsers() +{ + if ( 0 == m_vecUsers.Count() ) + return; + + PreRemoveAllUsers(); + + // Iterate all the users and tell them to leave this server. + // Using back because the users will remove themselves from + // this list during this function + FOR_EACH_VEC_BACK( m_vecUsers, i ) + { + CGCUserSession *pUserSession = GGCBase()->FindUserSession( m_vecUsers[i] ); + if ( pUserSession ) + { + pUserSession->BLeaveServer(); + } + } + + // Catch anyone we don't have a session for anymore + m_vecUsers.RemoveAll(); + + PostRemoveAllUsers(); +} + +#define iptod(x) ((x)>>24&0xff), ((x)>>16&0xff), ((x)>>8&0xff), ((x)&0xff) + +//----------------------------------------------------------------------------- +// Purpose: Dumps useful information about this session +//----------------------------------------------------------------------------- +void CGCGSSession::Dump( bool bFull ) const +{ +// this is ifdef'd out in Steam because GCSDK can't depend on steamid.cpp +#ifndef STEAM + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "GS Session %s\n", GetSteamID().Render() ); + CJob *pJob = GGCBase()->PJobHoldingLock( GetSteamID() ); + if( pJob ) + { + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\t LOCKED BY: %s\n", pJob->GetName() ); + } + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\t%d users:\n", m_vecUsers.Count() ); + FOR_EACH_VEC( m_vecUsers, nUser ) + { + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\t\t%s\n", m_vecUsers[nUser].Render() ); + } + if( GetSOCache() ) + { + if ( bFull ) + { + GetSOCache()->Dump(); + } + else + { + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\t SO Cache Version: %llu\n", GetSOCache()->GetVersion() ); + } + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Claims all the memory for the session object +//----------------------------------------------------------------------------- +#ifdef DBGFLAG_VALIDATE +void CGCGSSession::Validate( CValidator &validator, const char *pchName ) +{ + CGCSession::Validate( validator, pchName); + + VALIDATE_SCOPE(); + + ValidateObj( m_vecUsers ); +} +#endif // DBGFLAG_VALIDATE + + +//----------------------------------------------------------------------------- +// Purpose: Client says it needs the SO Cache +// Input : pNetPacket - received message +//----------------------------------------------------------------------------- +class CGCCacheSubscriptionRefresh: public CGCJob +{ +public: + CGCCacheSubscriptionRefresh( CGCBase *pGC ) : CGCJob( pGC ) { } + bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket ); +}; + +bool CGCCacheSubscriptionRefresh::BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket ) +{ + CProtoBufMsg<CMsgSOCacheSubscriptionRefresh> msg( pNetPacket ); + + CSteamID steamID( msg.Hdr().client_steam_id() ); + + CSteamID steamIDCacheOwner( msg.Body().owner() ); + CGCSharedObjectCache *pCache = m_pGC->FindSOCache( steamIDCacheOwner ); + if ( pCache == NULL || !pCache->BIsSubscribed( steamID ) ) + { + return false; + } + + pCache->SendSubscriberMessage( steamID ); + + return true; +} + +GC_REG_JOB( CGCBase, CGCCacheSubscriptionRefresh, "CGCCacheSubscriptionRefresh", k_ESOMsg_CacheSubscriptionRefresh, k_EServerTypeGC ); + +} // namespace GCSDK + |