PageRenderTime 60ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/SteamKit2/SteamKit2/Steam3/CMClient.cs

https://bitbucket.org/VoiDeD/steamre/
C# | 509 lines | 304 code | 89 blank | 116 comment | 25 complexity | 01d4823cd73b90dcd72f3235f706991b MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, Apache-2.0, BSD-3-Clause
  1. /*
  2. * This file is subject to the terms and conditions defined in
  3. * file 'license.txt', which is part of this source code package.
  4. */
  5. using System;
  6. using System.Collections.Generic;
  7. using System.IO;
  8. using System.Net;
  9. using System.Net.Sockets;
  10. using System.Collections.ObjectModel;
  11. using System.Linq;
  12. namespace SteamKit2.Internal
  13. {
  14. /// <summary>
  15. /// This base client handles the underlying connection to a CM server. This class should not be use directly, but through the <see cref="SteamClient"/> class.
  16. /// </summary>
  17. public abstract class CMClient
  18. {
  19. /// <summary>
  20. /// Bootstrap list of CM servers.
  21. /// </summary>
  22. public static ReadOnlyCollection<IPEndPoint> Servers { get; private set; }
  23. /// <summary>
  24. /// Returns the the local IP of this client.
  25. /// </summary>
  26. /// <returns>The local IP.</returns>
  27. public IPAddress LocalIP
  28. {
  29. get { return connection.GetLocalIP(); }
  30. }
  31. /// <summary>
  32. /// Gets the connected universe of this client.
  33. /// This value will be <see cref="EUniverse.Invalid"/> if the client is logged off of Steam.
  34. /// </summary>
  35. /// <value>The universe.</value>
  36. public EUniverse ConnectedUniverse { get; private set; }
  37. /// <summary>
  38. /// Gets a value indicating whether this instance is connected to the remote CM server.
  39. /// </summary>
  40. /// <value>
  41. /// <c>true</c> if this instance is connected; otherwise, <c>false</c>.
  42. /// </value>
  43. public bool IsConnected { get { return ConnectedUniverse != EUniverse.Invalid; } }
  44. /// <summary>
  45. /// Gets the session token assigned to this client from the AM.
  46. /// </summary>
  47. public ulong SessionToken { get; private set; }
  48. /// <summary>
  49. /// Gets the session ID of this client. This value is assigned after a logon attempt has succeeded.
  50. /// This value will be <c>null</c> if the client is logged off of Steam.
  51. /// </summary>
  52. /// <value>The session ID.</value>
  53. public int? SessionID { get; private set; }
  54. /// <summary>
  55. /// Gets the SteamID of this client. This value is assigned after a logon attempt has succeeded.
  56. /// This value will be <c>null</c> if the client is logged off of Steam.
  57. /// </summary>
  58. /// <value>The SteamID.</value>
  59. public SteamID SteamID { get; private set; }
  60. /// <summary>
  61. /// Gets or sets the connection timeout used when connecting to the Steam server.
  62. /// The default value is 5 seconds.
  63. /// </summary>
  64. /// <value>
  65. /// The connection timeout.
  66. /// </value>
  67. public TimeSpan ConnectionTimeout { get; set; }
  68. Connection connection;
  69. byte[] tempSessionKey;
  70. ScheduledFunction heartBeatFunc;
  71. Dictionary<EServerType, List<IPEndPoint>> serverMap;
  72. static CMClient()
  73. {
  74. Servers = new ReadOnlyCollection<IPEndPoint>( new List<IPEndPoint>
  75. {
  76. // Qwest, Seattle
  77. new IPEndPoint( IPAddress.Parse( "72.165.61.174" ), 27017 ),
  78. new IPEndPoint( IPAddress.Parse( "72.165.61.174" ), 27018 ),
  79. new IPEndPoint( IPAddress.Parse( "72.165.61.175" ), 27017 ),
  80. new IPEndPoint( IPAddress.Parse( "72.165.61.175" ), 27018 ),
  81. new IPEndPoint( IPAddress.Parse( "72.165.61.176" ), 27017 ),
  82. new IPEndPoint( IPAddress.Parse( "72.165.61.176" ), 27018 ),
  83. new IPEndPoint( IPAddress.Parse( "72.165.61.185" ), 27017 ),
  84. new IPEndPoint( IPAddress.Parse( "72.165.61.185" ), 27018 ),
  85. new IPEndPoint( IPAddress.Parse( "72.165.61.187" ), 27017 ),
  86. new IPEndPoint( IPAddress.Parse( "72.165.61.187" ), 27018 ),
  87. new IPEndPoint( IPAddress.Parse( "72.165.61.188" ), 27017 ),
  88. new IPEndPoint( IPAddress.Parse( "72.165.61.188" ), 27018 ),
  89. // Inteliquent, Luxembourg, cm-[01-04].lux.valve.net
  90. new IPEndPoint( IPAddress.Parse( "146.66.152.12" ), 27017 ),
  91. new IPEndPoint( IPAddress.Parse( "146.66.152.12" ), 27018 ),
  92. new IPEndPoint( IPAddress.Parse( "146.66.152.12" ), 27019 ),
  93. new IPEndPoint( IPAddress.Parse( "146.66.152.13" ), 27017 ),
  94. new IPEndPoint( IPAddress.Parse( "146.66.152.13" ), 27018 ),
  95. new IPEndPoint( IPAddress.Parse( "146.66.152.13" ), 27019 ),
  96. new IPEndPoint( IPAddress.Parse( "146.66.152.14" ), 27017 ),
  97. new IPEndPoint( IPAddress.Parse( "146.66.152.14" ), 27018 ),
  98. new IPEndPoint( IPAddress.Parse( "146.66.152.14" ), 27019 ),
  99. new IPEndPoint( IPAddress.Parse( "146.66.152.15" ), 27017 ),
  100. new IPEndPoint( IPAddress.Parse( "146.66.152.15" ), 27018 ),
  101. new IPEndPoint( IPAddress.Parse( "146.66.152.15" ), 27019 ),
  102. /* Highwinds, Netherlands (not live)
  103. new IPEndPoint( IPAddress.Parse( "81.171.115.5" ), 27017 ),
  104. new IPEndPoint( IPAddress.Parse( "81.171.115.5" ), 27018 ),
  105. new IPEndPoint( IPAddress.Parse( "81.171.115.5" ), 27019 ),
  106. new IPEndPoint( IPAddress.Parse( "81.171.115.6" ), 27017 ),
  107. new IPEndPoint( IPAddress.Parse( "81.171.115.6" ), 27018 ),
  108. new IPEndPoint( IPAddress.Parse( "81.171.115.6" ), 27019 ),
  109. new IPEndPoint( IPAddress.Parse( "81.171.115.7" ), 27017 ),
  110. new IPEndPoint( IPAddress.Parse( "81.171.115.7" ), 27018 ),
  111. new IPEndPoint( IPAddress.Parse( "81.171.115.7" ), 27019 ),
  112. new IPEndPoint( IPAddress.Parse( "81.171.115.8" ), 27017 ),
  113. new IPEndPoint( IPAddress.Parse( "81.171.115.8" ), 27018 ),
  114. new IPEndPoint( IPAddress.Parse( "81.171.115.8" ), 27019 ),*/
  115. // Highwinds, Kaysville
  116. new IPEndPoint( IPAddress.Parse( "209.197.29.196" ), 27017 ),
  117. new IPEndPoint( IPAddress.Parse( "209.197.29.197" ), 27017 ),
  118. /* Starhub, Singapore (non-optimal route)
  119. new IPEndPoint( IPAddress.Parse( "103.28.54.10" ), 27017 ),
  120. new IPEndPoint( IPAddress.Parse( "103.28.54.11" ), 27017 )*/
  121. } );
  122. }
  123. /// <summary>
  124. /// Initializes a new instance of the <see cref="CMClient"/> class with a specific connection type.
  125. /// </summary>
  126. /// <param name="type">The connection type to use.</param>
  127. /// <exception cref="NotSupportedException">
  128. /// The provided <see cref="ProtocolType"/> is not supported.
  129. /// Only Tcp and Udp are available.
  130. /// </exception>
  131. public CMClient( ProtocolType type = ProtocolType.Tcp )
  132. {
  133. serverMap = new Dictionary<EServerType, List<IPEndPoint>>();
  134. // our default timeout
  135. ConnectionTimeout = TimeSpan.FromSeconds( 5 );
  136. switch ( type )
  137. {
  138. case ProtocolType.Tcp:
  139. connection = new TcpConnection();
  140. break;
  141. case ProtocolType.Udp:
  142. connection = new UdpConnection();
  143. break;
  144. default:
  145. throw new NotSupportedException( "The provided protocol type is not supported. Only Tcp and Udp are available." );
  146. }
  147. connection.NetMsgReceived += NetMsgReceived;
  148. connection.Disconnected += Disconnected;
  149. heartBeatFunc = new ScheduledFunction( () =>
  150. {
  151. Send( new ClientMsgProtobuf<CMsgClientHeartBeat>( EMsg.ClientHeartBeat ) );
  152. } );
  153. }
  154. /// <summary>
  155. /// Connects this client to a Steam3 server.
  156. /// This begins the process of connecting and encrypting the data channel between the client and the server.
  157. /// Results are returned asynchronously in a <see cref="SteamClient.ConnectedCallback"/>.
  158. /// If the server that SteamKit attempts to connect to is down, a <see cref="SteamClient.DisconnectedCallback"/>
  159. /// will be posted instead.
  160. /// SteamKit will not attempt to reconnect to Steam, you must handle this callback and call Connect again
  161. /// preferrably after a short delay.
  162. /// </summary>
  163. /// <param name="cmServer">
  164. /// The <see cref="IPEndPoint"/> of the CM server to connect to.
  165. /// If <c>null</c>, SteamKit will randomly select a CM server from its internal list.
  166. /// </param>
  167. public void Connect( IPEndPoint cmServer = null )
  168. {
  169. this.Disconnect();
  170. if ( cmServer == null )
  171. {
  172. var serverList = Servers;
  173. Random random = new Random();
  174. cmServer = serverList[ random.Next( serverList.Count ) ];
  175. }
  176. connection.Connect( cmServer, ( int )ConnectionTimeout.TotalMilliseconds );
  177. }
  178. /// <summary>
  179. /// Disconnects this client.
  180. /// </summary>
  181. public void Disconnect()
  182. {
  183. heartBeatFunc.Stop();
  184. connection.Disconnect();
  185. }
  186. /// <summary>
  187. /// Sends the specified client message to the server.
  188. /// This method automatically assigns the correct SessionID and SteamID of the message.
  189. /// </summary>
  190. /// <param name="msg">The client message to send.</param>
  191. public void Send( IClientMsg msg )
  192. {
  193. if ( this.SessionID.HasValue )
  194. msg.SessionID = this.SessionID.Value;
  195. if ( this.SteamID != null )
  196. msg.SteamID = this.SteamID;
  197. DebugLog.WriteLine( "CMClient", "Sent -> EMsg: {0} (Proto: {1})", msg.MsgType, msg.IsProto );
  198. // we'll swallow any network failures here because they will be thrown later
  199. // on the network thread, and that will lead to a disconnect callback
  200. // down the line
  201. try
  202. {
  203. connection.Send( msg );
  204. }
  205. catch ( IOException )
  206. {
  207. }
  208. catch ( SocketException )
  209. {
  210. }
  211. }
  212. /// <summary>
  213. /// Returns the list of servers matching the given type
  214. /// </summary>
  215. /// <param name="type">Server type requested</param>
  216. /// <returns>List of server endpoints</returns>
  217. public List<IPEndPoint> GetServersOfType( EServerType type )
  218. {
  219. List<IPEndPoint> list;
  220. if ( !serverMap.TryGetValue( type, out list ) )
  221. return new List<IPEndPoint>();
  222. return list;
  223. }
  224. /// <summary>
  225. /// Called when a client message is received from the network.
  226. /// </summary>
  227. /// <param name="packetMsg">The packet message.</param>
  228. protected virtual void OnClientMsgReceived( IPacketMsg packetMsg )
  229. {
  230. DebugLog.WriteLine( "CMClient", "<- Recv'd EMsg: {0} ({1}) (Proto: {2})", packetMsg.MsgType, ( int )packetMsg.MsgType, packetMsg.IsProto );
  231. switch ( packetMsg.MsgType )
  232. {
  233. case EMsg.ChannelEncryptRequest:
  234. HandleEncryptRequest( packetMsg );
  235. break;
  236. case EMsg.ChannelEncryptResult:
  237. HandleEncryptResult( packetMsg );
  238. break;
  239. case EMsg.Multi:
  240. HandleMulti( packetMsg );
  241. break;
  242. case EMsg.ClientLogOnResponse: // we handle this to get the SteamID/SessionID and to setup heartbeating
  243. HandleLogOnResponse( packetMsg );
  244. break;
  245. case EMsg.ClientLoggedOff: // to stop heartbeating when we get logged off
  246. HandleLoggedOff( packetMsg );
  247. break;
  248. case EMsg.ClientServerList: // Steam server list
  249. HandleServerList( packetMsg );
  250. break;
  251. case EMsg.ClientCMList:
  252. HandleCMList( packetMsg );
  253. break;
  254. case EMsg.ClientSessionToken: // am session token
  255. HandleSessionToken( packetMsg );
  256. break;
  257. }
  258. }
  259. /// <summary>
  260. /// Called when the client is physically disconnected from Steam3.
  261. /// </summary>
  262. protected abstract void OnClientDisconnected();
  263. void NetMsgReceived( object sender, NetMsgEventArgs e )
  264. {
  265. OnClientMsgReceived( GetPacketMsg( e.Data ) );
  266. }
  267. void Disconnected( object sender, EventArgs e )
  268. {
  269. ConnectedUniverse = EUniverse.Invalid;
  270. heartBeatFunc.Stop();
  271. connection.NetFilter = null;
  272. OnClientDisconnected();
  273. }
  274. internal static IPacketMsg GetPacketMsg( byte[] data )
  275. {
  276. uint rawEMsg = BitConverter.ToUInt32( data, 0 );
  277. EMsg eMsg = MsgUtil.GetMsg( rawEMsg );
  278. switch ( eMsg )
  279. {
  280. // certain message types are always MsgHdr
  281. case EMsg.ChannelEncryptRequest:
  282. case EMsg.ChannelEncryptResponse:
  283. case EMsg.ChannelEncryptResult:
  284. return new PacketMsg( eMsg, data );
  285. }
  286. if ( MsgUtil.IsProtoBuf( rawEMsg ) )
  287. {
  288. // if the emsg is flagged, we're a proto message
  289. return new PacketClientMsgProtobuf( eMsg, data );
  290. }
  291. else
  292. {
  293. // otherwise we're a struct message
  294. return new PacketClientMsg( eMsg, data );
  295. }
  296. }
  297. #region ClientMsg Handlers
  298. void HandleMulti( IPacketMsg packetMsg )
  299. {
  300. if ( !packetMsg.IsProto )
  301. {
  302. DebugLog.WriteLine( "CMClient", "HandleMulti got non-proto MsgMulti!!" );
  303. return;
  304. }
  305. var msgMulti = new ClientMsgProtobuf<CMsgMulti>( packetMsg );
  306. byte[] payload = msgMulti.Body.message_body;
  307. if ( msgMulti.Body.size_unzipped > 0 )
  308. {
  309. try
  310. {
  311. payload = ZipUtil.Decompress( payload );
  312. }
  313. catch ( Exception ex )
  314. {
  315. DebugLog.WriteLine( "CMClient", "HandleMulti encountered an exception when decompressing.\n{0}", ex.ToString() );
  316. return;
  317. }
  318. }
  319. DataStream ds = new DataStream( payload );
  320. while ( ds.SizeRemaining() != 0 )
  321. {
  322. uint subSize = ds.ReadUInt32();
  323. byte[] subData = ds.ReadBytes( subSize );
  324. OnClientMsgReceived( GetPacketMsg( subData ) );
  325. }
  326. }
  327. void HandleLogOnResponse( IPacketMsg packetMsg )
  328. {
  329. if ( !packetMsg.IsProto )
  330. {
  331. // a non proto ClientLogonResponse can come in as a result of connecting but never sending a ClientLogon
  332. // in this case, it always fails, so we don't need to do anything special here
  333. DebugLog.WriteLine( "CMClient", "Got non-proto logon response, this is indicative of no logon attempt after connecting." );
  334. return;
  335. }
  336. var logonResp = new ClientMsgProtobuf<CMsgClientLogonResponse>( packetMsg );
  337. if ( logonResp.Body.eresult == ( int )EResult.OK )
  338. {
  339. SessionID = logonResp.ProtoHeader.client_sessionid;
  340. SteamID = logonResp.ProtoHeader.steamid;
  341. int hbDelay = logonResp.Body.out_of_game_heartbeat_seconds;
  342. // restart heartbeat
  343. heartBeatFunc.Stop();
  344. heartBeatFunc.Delay = TimeSpan.FromSeconds( hbDelay );
  345. heartBeatFunc.Start();
  346. }
  347. }
  348. void HandleEncryptRequest( IPacketMsg packetMsg )
  349. {
  350. var encRequest = new Msg<MsgChannelEncryptRequest>( packetMsg );
  351. EUniverse eUniv = encRequest.Body.Universe;
  352. uint protoVersion = encRequest.Body.ProtocolVersion;
  353. DebugLog.WriteLine( "CMClient", "Got encryption request. Universe: {0} Protocol ver: {1}", eUniv, protoVersion );
  354. DebugLog.Assert( protoVersion == 1, "CMClient", "Encryption handshake protocol version mismatch!" );
  355. byte[] pubKey = KeyDictionary.GetPublicKey( eUniv );
  356. if ( pubKey == null )
  357. {
  358. DebugLog.WriteLine( "CMClient", "HandleEncryptionRequest got request for invalid universe! Universe: {0} Protocol ver: {1}", eUniv, protoVersion );
  359. return;
  360. }
  361. ConnectedUniverse = eUniv;
  362. var encResp = new Msg<MsgChannelEncryptResponse>();
  363. tempSessionKey = CryptoHelper.GenerateRandomBlock( 32 );
  364. byte[] cryptedSessKey = null;
  365. using ( var rsa = new RSACrypto( pubKey ) )
  366. {
  367. cryptedSessKey = rsa.Encrypt( tempSessionKey );
  368. }
  369. byte[] keyCrc = CryptoHelper.CRCHash( cryptedSessKey );
  370. encResp.Write( cryptedSessKey );
  371. encResp.Write( keyCrc );
  372. encResp.Write( ( uint )0 );
  373. this.Send( encResp );
  374. }
  375. void HandleEncryptResult( IPacketMsg packetMsg )
  376. {
  377. var encResult = new Msg<MsgChannelEncryptResult>( packetMsg );
  378. DebugLog.WriteLine( "CMClient", "Encryption result: {0}", encResult.Body.Result );
  379. if ( encResult.Body.Result == EResult.OK )
  380. {
  381. connection.NetFilter = new NetFilterEncryption( tempSessionKey );
  382. }
  383. }
  384. void HandleLoggedOff( IPacketMsg packetMsg )
  385. {
  386. SessionID = null;
  387. SteamID = null;
  388. heartBeatFunc.Stop();
  389. }
  390. void HandleServerList( IPacketMsg packetMsg )
  391. {
  392. var listMsg = new ClientMsgProtobuf<CMsgClientServerList>( packetMsg );
  393. foreach ( var server in listMsg.Body.servers )
  394. {
  395. EServerType type = ( EServerType )server.server_type;
  396. List<IPEndPoint> endpointList;
  397. if ( !serverMap.TryGetValue( type, out endpointList ) )
  398. {
  399. serverMap[ type ] = endpointList = new List<IPEndPoint>();
  400. }
  401. endpointList.Add( new IPEndPoint( NetHelpers.GetIPAddress( server.server_ip ), ( int )server.server_port ) );
  402. }
  403. }
  404. void HandleCMList( IPacketMsg packetMsg )
  405. {
  406. var cmMsg = new ClientMsgProtobuf<CMsgClientCMList>( packetMsg );
  407. DebugLog.Assert( cmMsg.Body.cm_addresses.Count == cmMsg.Body.cm_ports.Count, "CMClient", "HandleCMList received malformed message" );
  408. var cmList = cmMsg.Body.cm_addresses
  409. .Zip( cmMsg.Body.cm_ports, ( addr, port ) => new IPEndPoint( NetHelpers.GetIPAddress( addr ), ( int )port ) );
  410. // update our bootstrap list with steam's list of CMs
  411. Servers = new ReadOnlyCollection<IPEndPoint>( cmList.ToList() );
  412. }
  413. void HandleSessionToken( IPacketMsg packetMsg )
  414. {
  415. var sessToken = new ClientMsgProtobuf<CMsgClientSessionToken>( packetMsg );
  416. SessionToken = sessToken.Body.token;
  417. }
  418. #endregion
  419. }
  420. }