Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

358 lines
13 KiB

  1. //========= Copyright � Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose: Forward server log lines to remote listeners
  4. //
  5. //=============================================================================//
  6. #include "cbase.h"
  7. #include "server_log_http_dispatcher.h"
  8. #include "gameinterface.h"
  9. #include "matchmaking/imatchframework.h"
  10. #include "engine/inetsupport.h"
  11. // memdbgon must be the last include file in a .cpp file!!!
  12. #include "tier0/memdbgon.h"
  13. class CServerLogDestination
  14. {
  15. public:
  16. CServerLogDestination( const char* szDestAddress )
  17. {
  18. m_sURI.Set( szDestAddress );
  19. m_flTimeout = 10.0f;
  20. m_hHTTPRequestHandle = INVALID_HTTPREQUEST_HANDLE;
  21. m_bIsFinished = false;
  22. m_unMaxUpdateStringSizeBytes = 250 * k_nKiloByte;
  23. m_lluUniqueToken = 0llu;
  24. static ConVarRef rev_LogAddrSecret( "logaddress_token_secret" );
  25. const char* szSecret = rev_LogAddrSecret.GetString();
  26. // HACK: Same code in sv_log, but not worth plumbing that through to share for this kinda feature... Doesn't really matter if they drift
  27. if ( szSecret && !StringIsEmpty( szSecret ) )
  28. {
  29. CRC64_ProcessBuffer( &m_lluUniqueToken, szSecret, V_strlen( szSecret ) );
  30. if ( !m_lluUniqueToken )
  31. m_lluUniqueToken = 1;
  32. }
  33. Reset();
  34. }
  35. ~CServerLogDestination()
  36. {
  37. if ( m_hHTTPRequestHandle )
  38. steamgameserverapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle );
  39. }
  40. bool BRequestPending( void ) const { return m_hHTTPRequestHandle != INVALID_HTTPREQUEST_HANDLE; }
  41. bool BIsFinished( void ) const { return m_bIsFinished; }
  42. bool SendUpdate( int32 iFromTIck, int32 iToTick, CServerLogHTTPDispatcher::LogLinesList_t::IndexType_t idxEndOfUpdate, const char* pLogLines, size_t unUpdateStrLenBytes );
  43. CServerLogHTTPDispatcher::LogLinesList_t::IndexType_t LastUpdateIndex() const { return m_idxLastAcknowledgedUpdate; }
  44. void Reset( void ) // Called during cleanup between maps or at shutdown
  45. {
  46. m_idxPendingUpdate = CServerLogHTTPDispatcher::LogLinesList_t::InvalidIndex();
  47. m_idxLastAcknowledgedUpdate = CServerLogHTTPDispatcher::LogLinesList_t::InvalidIndex();
  48. if ( m_hHTTPRequestHandle )
  49. steamgameserverapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle );
  50. m_hHTTPRequestHandle = NULL;
  51. }
  52. size_t MaxUpdateStringSizeBytes() const { return m_unMaxUpdateStringSizeBytes; }
  53. void DumpStatusToConsole( void ) const;
  54. private:
  55. void Steam_OnHTTPRequestCompleted( HTTPRequestCompleted_t *p, bool bError );
  56. CUtlString m_sURI; // Base url for this destination
  57. // Keep track of the linked list node from the last successful update so we can continue where we left off
  58. CServerLogHTTPDispatcher::LogLinesList_t::IndexType_t m_idxLastAcknowledgedUpdate;
  59. CServerLogHTTPDispatcher::LogLinesList_t::IndexType_t m_idxPendingUpdate;
  60. HTTPRequestHandle m_hHTTPRequestHandle; // Pending request if any
  61. float m_flTimeout; // Timeout passed to steamhttp callbacks
  62. size_t m_unMaxUpdateStringSizeBytes;
  63. bool m_bIsFinished; // This listener is done for some reason, remove us from queue next update
  64. uint64 m_lluUniqueToken; // Some unique string set before adding a listener to be sent along with the request. Here to match behavior of udp system.
  65. // SteamHTTP members for callback handling
  66. CCallResult< CServerLogDestination, HTTPRequestCompleted_t > m_CallbackOnHTTPRequestCompleted;
  67. };
  68. static CServerLogHTTPDispatcher g_ServerLogHTTPDispatcher;
  69. CServerLogHTTPDispatcher* GetServerLogHTTPDispatcher( void ) { return &g_ServerLogHTTPDispatcher; }
  70. CON_COMMAND( logaddress_add_http, "Set URI of a listener to receive logs via http post. Wrap URI in double quotes." )
  71. {
  72. if ( args.ArgC() != 2 )
  73. {
  74. ConMsg( "logaddress_add_http: Invalid parameters, must be URI of a listener to receive logs wrapped in quotes, eg \"http://127.0.0.1:3001\"\n" );
  75. return;
  76. }
  77. GetServerLogHTTPDispatcher()->AddListener( args[ 1 ] );
  78. }
  79. CON_COMMAND( logaddress_delall_http, "Remove all http listeners from the dispatch list." )
  80. {
  81. GetServerLogHTTPDispatcher()->RemoveAllListeners();
  82. }
  83. CON_COMMAND( logaddress_list_http, "List all URIs currently receiving server logs" )
  84. {
  85. GetServerLogHTTPDispatcher()->DumpListenersToConsole();
  86. }
  87. CServerLogHTTPDispatcher::CServerLogHTTPDispatcher():
  88. CAutoGameSystemPerFrame( "ServerLogHTTPDispatcher" )
  89. {
  90. Reset();
  91. }
  92. void CServerLogHTTPDispatcher::BuildTimestampString( void )
  93. {
  94. double flTimeSpentLogging = Plat_FloatTime() - m_loggingStartPlatTime;
  95. tm now;
  96. Plat_ConvertToLocalTime( ( m_localTimeLoggingStart + (uint32)flTimeSpentLogging ), &now );
  97. uint32 msecs = ( flTimeSpentLogging - ( uint32 )flTimeSpentLogging ) * 1000;
  98. m_strTimeStampPrefix.Format( "%02u/%02u/%04u - %02u:%02u:%02u.%03u",
  99. now.tm_mon + 1, now.tm_mday, 1900 + now.tm_year,
  100. now.tm_hour, now.tm_min, now.tm_sec, msecs );
  101. }
  102. bool CServerLogHTTPDispatcher::LogForHTTPListeners( const char* szLogLine )
  103. {
  104. if ( !szLogLine || StringIsEmpty( szLogLine ) )
  105. return false;
  106. if ( m_strLogLinesThisTick.IsEmpty() )
  107. {
  108. BuildTimestampString();
  109. }
  110. m_strLogLinesThisTick.AppendFormat( "%s - %s", m_strTimeStampPrefix.Get(), szLogLine );
  111. return true;
  112. }
  113. void CServerLogHTTPDispatcher::AddListener( const char* szURI )
  114. {
  115. m_vecListeners.AddToTail( new CServerLogDestination( szURI ) );
  116. }
  117. void CServerLogHTTPDispatcher::RemoveAllListeners( void )
  118. {
  119. m_vecListeners.PurgeAndDeleteElements();
  120. }
  121. void CServerLogHTTPDispatcher::DumpListenersToConsole( void ) const
  122. {
  123. FOR_EACH_VEC( m_vecListeners, i )
  124. {
  125. CServerLogDestination *pDest = m_vecListeners[ i ];
  126. if ( pDest )
  127. {
  128. pDest->DumpStatusToConsole();
  129. ConMsg( "\tlast acknowledged tick %d\n", m_llLogPerTick[ pDest->LastUpdateIndex() ].iTick );
  130. }
  131. }
  132. }
  133. void CServerLogHTTPDispatcher::Shutdown()
  134. {
  135. Reset();
  136. m_vecListeners.PurgeAndDeleteElements();
  137. }
  138. void CServerLogHTTPDispatcher::LevelInitPreEntity()
  139. {
  140. #if 0 // autoadd self for debugging
  141. if ( m_vecListeners.Count() == 0 )
  142. engine->ServerCommand( "log on;logaddress_add_http \"http://127.0.0.1:3000\"\n" );
  143. #endif
  144. m_localTimeLoggingStart = Plat_GetTime();
  145. m_loggingStartPlatTime = Plat_FloatTime();
  146. }
  147. void CServerLogHTTPDispatcher::LevelShutdownPreEntity()
  148. {
  149. Reset();
  150. }
  151. void CServerLogHTTPDispatcher::Reset( void )
  152. {
  153. m_strServerLog.Clear();
  154. m_llLogPerTick.Purge();
  155. m_strLogLinesThisTick.Clear();
  156. FOR_EACH_VEC( m_vecListeners, i )
  157. {
  158. m_vecListeners[i]->Reset();
  159. }
  160. #if defined DBGFLAG_ASSERT
  161. m_dbgLastAllocedHandle = LogLinesList_t::InvalidIndex();
  162. #endif
  163. }
  164. void CServerLogHTTPDispatcher::PreClientUpdate()
  165. {
  166. if ( m_vecListeners.Count() == 0 )
  167. return;
  168. // Can't do any work before we have steamhttp
  169. if ( steamgameserverapicontext == NULL || !steamgameserverapicontext->SteamHTTP() || !steamgameserverapicontext->SteamGameServer() )
  170. return;
  171. AssertOnce( engine->IsLogEnabled() );
  172. static bool bWarnOnce = false;
  173. if ( !engine->IsLogEnabled() && !bWarnOnce )
  174. {
  175. Warning( "Server log http listener registered, but logging is not turned on for this server! Start recording the log before registering any http destinations\n" );
  176. bWarnOnce = true;
  177. return;
  178. }
  179. // If we've crossed a tick boundary, add the spew for that period to the in-memory log
  180. int32 iTickLastLogged = m_llLogPerTick.Tail() != LogLinesList_t::InvalidIndex() ? m_llLogPerTick [ m_llLogPerTick.Tail() ].iTick : 0;
  181. if ( gpGlobals->tickcount > iTickLastLogged && !m_strLogLinesThisTick.IsEmpty() )
  182. {
  183. // Record offset from start of our log string for this tick's lines and its length.
  184. LogLinesList_t::IndexType_t newIdx = m_llLogPerTick.AddToTail( LogLineStartForTick_t ( gpGlobals->tickcount, m_strServerLog.Length(), m_strLogLinesThisTick.Length() ) );
  185. // Never reuse handles in this LL unless we tear down the log between levels or otherwise know there are no listeners holding on to them
  186. NOTE_UNUSED( newIdx );
  187. #if defined DBGFLAG_ASSERT
  188. Assert( newIdx > m_dbgLastAllocedHandle || m_dbgLastAllocedHandle == LogLinesList_t::InvalidIndex() );
  189. m_dbgLastAllocedHandle = newIdx;
  190. #endif
  191. // Append this tick's lines to the log and clear
  192. m_strServerLog.Append( m_strLogLinesThisTick.Get() );
  193. // Don't free memory for this as it's a frequently used bucket. Keep alloc at whatever high water mark its reached until level transition
  194. m_strLogLinesThisTick.SetLength( 0 );
  195. // Mark this as the most recent tick for any updates being sent below
  196. iTickLastLogged = gpGlobals->tickcount;
  197. }
  198. if ( m_llLogPerTick.Tail() != LogLinesList_t::InvalidIndex() )
  199. {
  200. FOR_EACH_VEC_BACK( m_vecListeners, i )
  201. {
  202. CServerLogDestination* pDest = m_vecListeners[ i ];
  203. if ( pDest->BRequestPending() ) // Skip requests still waiting to hear back or time out
  204. continue;
  205. if ( pDest->BIsFinished() ) // Clean up listeners no longer needing updates
  206. {
  207. delete m_vecListeners[ i ];
  208. m_vecListeners.Remove( i );
  209. continue;
  210. }
  211. // If we have new lines for this listener, send an update
  212. LogLinesList_t::IndexType_t idxLast = pDest->LastUpdateIndex();
  213. if ( idxLast == m_llLogPerTick.Tail() )
  214. continue; // they have the latest
  215. LogLinesList_t::IndexType_t idxFrom = LogLinesList_t::InvalidIndex();
  216. if ( idxLast == LogLinesList_t::InvalidIndex() )
  217. idxFrom = m_llLogPerTick.Head(); // never acknowledged an update
  218. else
  219. idxFrom = m_llLogPerTick.Next( idxLast ); // start with the next entry after their last acknowledgment
  220. Assert( idxFrom != LogLinesList_t::InvalidIndex() );
  221. LogLineStartForTick_t &updateStart = m_llLogPerTick[ idxFrom ];
  222. Assert( updateStart.iTick <= iTickLastLogged );
  223. LogLinesList_t::IndexType_t idxTo = idxFrom;
  224. size_t unUpdateStringSize = updateStart.unLength;
  225. while ( unUpdateStringSize < pDest->MaxUpdateStringSizeBytes() && m_llLogPerTick.Next( idxTo ) != LogLinesList_t::InvalidIndex() )
  226. {
  227. idxTo = m_llLogPerTick.Next( idxTo );
  228. unUpdateStringSize += m_llLogPerTick[ idxTo ].unLength;
  229. }
  230. Assert( idxFrom != LogLinesList_t::InvalidIndex() && idxTo != LogLinesList_t::InvalidIndex() );
  231. pDest->SendUpdate( updateStart.iTick, m_llLogPerTick[idxTo].iTick, idxTo , m_strServerLog.Get() + updateStart.unOffsetStart, unUpdateStringSize );
  232. }
  233. }
  234. }
  235. bool CServerLogDestination::SendUpdate( int32 iFromTick, int32 iToTick, CServerLogHTTPDispatcher::LogLinesList_t::IndexType_t idxEndOfUpdate, const char* pLogLines, size_t unUpdateStrLenBytes )
  236. {
  237. m_hHTTPRequestHandle = steamgameserverapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodPOST, m_sURI.Get() );
  238. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestNetworkActivityTimeout( m_hHTTPRequestHandle, m_flTimeout );
  239. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-Tick-Start", CNumStr( iFromTick ).String() );
  240. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-Tick-End", CNumStr( iToTick ).String() );
  241. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-SteamID", steamgameserverapicontext->SteamGameServer()->GetSteamID().Render() );
  242. INetSupport::ServerInfo_t serverInfo;
  243. if ( INetSupport *pINetSupport = ( INetSupport * )g_pMatchFramework->GetMatchExtensions()->GetRegisteredExtensionInterface( INETSUPPORT_VERSION_STRING ) )
  244. {
  245. pINetSupport->GetServerInfo( &serverInfo );
  246. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-Server-Addr", serverInfo.m_netAdr.ToString() );
  247. }
  248. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-Timestamp", CNumStr( Plat_MSTime() ).String() );
  249. if ( m_lluUniqueToken )
  250. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hHTTPRequestHandle, "X-Server-Unique-Token", CNumStr( m_lluUniqueToken ).String() );
  251. steamgameserverapicontext->SteamHTTP()->SetHTTPRequestRawPostBody( m_hHTTPRequestHandle, "text/plain", ( uint8* )pLogLines, unUpdateStrLenBytes );
  252. SteamAPICall_t hCall = NULL;
  253. if ( m_hHTTPRequestHandle && steamgameserverapicontext->SteamHTTP()->SendHTTPRequest( m_hHTTPRequestHandle, &hCall ) && hCall )
  254. {
  255. m_CallbackOnHTTPRequestCompleted.Set( hCall, this, &CServerLogDestination::Steam_OnHTTPRequestCompleted );
  256. m_idxPendingUpdate = idxEndOfUpdate;
  257. }
  258. else
  259. {
  260. if ( m_hHTTPRequestHandle )
  261. steamgameserverapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hHTTPRequestHandle );
  262. m_hHTTPRequestHandle = NULL;
  263. Steam_OnHTTPRequestCompleted( NULL, true );
  264. return false;
  265. }
  266. return true;
  267. }
  268. void CServerLogDestination::DumpStatusToConsole( void ) const
  269. {
  270. ConMsg( "%s - %s pending request\n", m_sURI.Get(), BRequestPending() ? "has" : "no" );
  271. }
  272. void CServerLogDestination::Steam_OnHTTPRequestCompleted( HTTPRequestCompleted_t *p, bool bError )
  273. {
  274. if ( !m_hHTTPRequestHandle || !p || ( p->m_hRequest != m_hHTTPRequestHandle ) )
  275. return;
  276. bool bSuccess = !bError && p->m_eStatusCode == k_EHTTPStatusCode200OK;
  277. if ( bSuccess )
  278. {
  279. m_idxLastAcknowledgedUpdate = m_idxPendingUpdate;
  280. }
  281. else
  282. {
  283. switch ( p->m_eStatusCode )
  284. {
  285. case k_EHTTPStatusCodeInvalid:
  286. {
  287. Msg( "Internal HTTP error posting to url %s\n", m_sURI.Get() );
  288. }
  289. break;
  290. case k_EHTTPStatusCode410Gone:
  291. {
  292. Msg( "Listener %s sent code 410, removing from update list.\n", m_sURI.Get() );
  293. m_bIsFinished = true;
  294. }
  295. break;
  296. case k_EHTTPStatusCode205ResetContent:
  297. m_idxLastAcknowledgedUpdate = CServerLogHTTPDispatcher::LogLinesList_t::InvalidIndex(); // Destination is requesting entire log again
  298. break;
  299. default:
  300. {
  301. Msg( "Error posting to url %s, error code %d\n", m_sURI.Get(), p->m_eStatusCode );
  302. }
  303. break;
  304. }
  305. }
  306. m_idxPendingUpdate = CServerLogHTTPDispatcher::LogLinesList_t::InvalidIndex();
  307. steamgameserverapicontext->SteamHTTP()->ReleaseHTTPRequest( p->m_hRequest );
  308. m_hHTTPRequestHandle = NULL;
  309. }