PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/Utilities/Networking/BaseClient.cs

#
C# | 1140 lines | 584 code | 70 blank | 486 comment | 75 complexity | 4ebcead1a73053f1deb0e33db6df0f37 MD5 | raw file
Possible License(s): Apache-2.0
  1. // Helper to figure out networking issues, will write into console only!
  2. //#define LOG_STUFF
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Net.Sockets;
  7. using System.Threading;
  8. using Delta.Utilities.Compression;
  9. using Delta.Utilities.Cryptography;
  10. using Delta.Utilities.Helpers;
  11. using Delta.Utilities.Xml;
  12. namespace Delta.Utilities.Networking
  13. {
  14. /// <summary>
  15. /// Base class for connected clients. Contains most importantly the
  16. /// connected socket, but also some extra information like the username.
  17. /// Used both on the BaseServer side (then we just care about the socket
  18. /// and the basic data and networking functionality) and in the Client
  19. /// that connects to the BaseServer (then all features are used). Derive
  20. /// from this to add more functionality. If you just want to use this
  21. /// class the MessageReceived is the easiest way to receive network data.
  22. /// <para />
  23. /// This class also support encryption with AES and RSA to make network
  24. /// transmissions really secure. See the constructors for details.
  25. /// <para />
  26. /// Internally also used as a receiving data helper class by defining
  27. /// buffers for receiving data. Thanks to the IAsyncState passed around in
  28. /// StartReceivingMessages this data will be preserved as long as the client
  29. /// is connected. RememberLastData is important for partial message we might
  30. /// receive (if incoming data is bigger than receivedData can hold or if we
  31. /// get multiple messages at once or strange packet sizes, which is very
  32. /// normal for TCP packets).
  33. /// </summary>
  34. public abstract class BaseClient : IDisposable
  35. {
  36. #region Constants
  37. /// <summary>
  38. /// Number of basic message types (add this if you are planing to use
  39. /// the BasicMessageTypes plus own types in derived classes).
  40. /// </summary>
  41. public static readonly byte NumberOfBasicMessageTypes =
  42. (byte)EnumHelper.GetCount<BasicMessageTypes>();
  43. /// <summary>
  44. /// Default time to wait until we timeout a connection attempt. Since this
  45. /// runs in an extra thread we do not actually have to wait this long.
  46. /// The default wait time is 3 seconds, which should be enough for most
  47. /// users (even if a timeout happens, the user can try again).
  48. /// </summary>
  49. private const int DefaultConnectionTimeoutMs = 3000;
  50. /// <summary>
  51. /// Wait time in ms until we try to connect again if the connection failed.
  52. /// We wait for 1 seconds (3 in release mode) and then try again.
  53. /// More is too annoying when developing and this happens (because we
  54. /// might just started the server or just fixed our connection problems).
  55. /// </summary>
  56. private const int WaitTimeTillNextConnectionAttemptMs =
  57. 3000;
  58. #endregion
  59. #region Public
  60. /// <summary>
  61. /// Username for this client. We mostly only care on the server what the
  62. /// name of each connected client is (for identification and debugging).
  63. /// This is set after receiving the Connect message from the client in
  64. /// the OnConnected method and used quite a bit on servers and clients.
  65. /// More complex clients with username, passwords, emails, etc. obviously
  66. /// have more data and you don't have to use this at all (just override
  67. /// the Login methods).
  68. /// </summary>
  69. public string Username;
  70. /// <summary>
  71. /// Client socket if we are the server. If we are the client this is the
  72. /// server connection socket we send all data to.
  73. /// </summary>
  74. public Socket Socket
  75. {
  76. get;
  77. protected internal set;
  78. }
  79. /// <summary>
  80. /// Helper method to check if we are successfully logged in. Please do not
  81. /// use this one to check if we have received the login message because
  82. /// this will not be true if the server rejected us and disconnects us.
  83. /// Instead override the OnLogin method to see when we are connected or
  84. /// when we might be disconnected before this ever gets true.
  85. /// </summary>
  86. public bool SuccessfullyLoggedIn
  87. {
  88. get
  89. {
  90. return connected &&
  91. loggedIn;
  92. }
  93. }
  94. /// <summary>
  95. /// Address to the server. Only set in constructor, cannot be changed.
  96. /// Not used for BaseServer clients.
  97. /// </summary>
  98. protected string ServerAddress
  99. {
  100. get;
  101. private set;
  102. }
  103. /// <summary>
  104. /// List of server ports we can use to connect, they will be tried in this
  105. /// order (useful in case some ports are blocked, e.g. in company networks).
  106. /// Only set in constructor, cannot be changed. Not used for BaseServer
  107. /// clients.
  108. /// </summary>
  109. protected int[] ServerPorts
  110. {
  111. get;
  112. private set;
  113. }
  114. /// <summary>
  115. /// Is the connection made on the client side or is this just the
  116. /// connection from the server to the client (then this returns false).
  117. /// </summary>
  118. public bool IsClientSide
  119. {
  120. get
  121. {
  122. return linkToServer == null;
  123. }
  124. }
  125. #region OnDisconnected event
  126. /// <summary>
  127. /// Delegate for the BaseClient.OnDisconnected event.
  128. /// </summary>
  129. public delegate void DisconnectDelegate();
  130. /// <summary>
  131. /// Delegate to be notified when we lose the connection to the server.
  132. /// Note: This is only called when the server disconnects us (or we lose
  133. /// the network connection somehow), this is not called when we dispose
  134. /// the BaseClient via the Dispose method (which you can override if you
  135. /// want to handle normal disposes).
  136. /// </summary>
  137. public DisconnectDelegate OnDisconnected;
  138. #endregion
  139. #endregion
  140. #region Private
  141. /// <summary>
  142. /// Helper flag to remember if we have successfully connected (for server
  143. /// side client connections). Initially this is false and only after
  144. /// receiving the Connect (on the server) message with success we are
  145. /// fully connected (or once we established the connection on the client
  146. /// side). When not connected any attempt to receive any other message will
  147. /// result in failure and the client will immediately disconnect in the
  148. /// <see cref="OnRawMessageReceived"/> method.
  149. /// </summary>
  150. private bool connected;
  151. /// <summary>
  152. /// Is the client logged in? Initially false and will be set to true if
  153. /// the Login message has been received and is successful (both on the
  154. /// client and the server side). Any attempt to receive or send messages
  155. /// to a non connected and not loggedIn client will immediately result in
  156. /// a failure and the client will be disconnected.
  157. /// </summary>
  158. protected bool loggedIn;
  159. /// <summary>
  160. /// Maximum number of message types, set in constructor. Only used to
  161. /// provide checks if incoming data is valid and can be converted into
  162. /// a known message type with data.
  163. /// </summary>
  164. internal byte maxNumOfMessageTypes;
  165. /// <summary>
  166. /// Client buffer data, internal to allow SocketHelper access to this
  167. /// and protected to allow derived classes to override the
  168. /// StartReceivingMessagesAndSendConnectMessage method.
  169. /// </summary>
  170. protected internal byte[] clientBufferData =
  171. new byte[SocketHelper.ReceiveBufferSize];
  172. /// <summary>
  173. /// Remember last data, also only used for the SocketHelper class.
  174. /// </summary>
  175. internal byte[] rememberLastData;
  176. /// <summary>
  177. /// Helper thread to make connection initialization go as smoothly as
  178. /// possible. Note: This is only used on the client side (server already
  179. /// has a connected socket when creating a new Client instance).
  180. /// If we are still connecting sending out messages is delayed. If the
  181. /// connection is already established, we can send out messages directly
  182. /// (which is easier and quicker and this thread is not longer needed).
  183. /// </summary>
  184. private Thread connectionThread;
  185. /// <summary>
  186. /// Remember AES cryptography if this is wanted for the client server
  187. /// communication. Both the client and the server instance must be
  188. /// initialized with the same private AES key. The server has to wait
  189. /// until the Connected message arrives from the client to setup the
  190. /// Cryptography class with this same private key and seed value.
  191. /// </summary>
  192. private AES aes;
  193. /// <summary>
  194. /// You can even use an public RSA key to encrypt Connect messages, which
  195. /// can be decrypted the matching secret RSA key on the server. If this
  196. /// variable is null, no RSA cryptography is used.
  197. /// </summary>
  198. private readonly RSA rsa;
  199. /// <summary>
  200. /// Link to the BaseServer if this is a connected client on the server.
  201. /// Needed to check if cryptography data is valid and to decrypt it.
  202. /// </summary>
  203. private readonly BaseServer linkToServer;
  204. /// <summary>
  205. /// Helper list to send out messages in case we are not connected yet!
  206. /// </summary>
  207. private readonly List<byte> remToSendMessageType = new List<byte>();
  208. /// <summary>
  209. /// Helper list for the data of messages to be send out.
  210. /// </summary>
  211. private readonly List<byte[]> remToSendMessageData = new List<byte[]>();
  212. /// <summary>
  213. /// Helper list for the data of messages to be send out if compression
  214. /// should be enabled or not (default is true).
  215. /// </summary>
  216. private readonly List<bool> remToSendMessageCompression = new List<bool>();
  217. #endregion
  218. #region Constructors
  219. #region Constructor (server side)
  220. /// <summary>
  221. /// Create client instance with an already known connected socket. Only
  222. /// used on the server side for client connections. The client side has to
  223. /// use the other constructor to get connected to the server.
  224. /// This constructor will cause the ClientSide property to return false.
  225. /// </summary>
  226. /// <param name="setMaxNumOfMessageTypes">Set max number of message types,
  227. /// which is the same value as the server uses.</param>
  228. /// <param name="setClientSocket">Get client socket</param>
  229. /// <param name="setLinkToServer">
  230. /// Link to the BaseServer if this is a connected client on the server.
  231. /// Needed to check if cryptography data is valid and to decrypt.
  232. /// </param>
  233. protected BaseClient(Socket setClientSocket, byte setMaxNumOfMessageTypes,
  234. BaseServer setLinkToServer)
  235. {
  236. Socket = setClientSocket;
  237. maxNumOfMessageTypes = setMaxNumOfMessageTypes;
  238. linkToServer = setLinkToServer;
  239. // Everything else is either not used or set later.
  240. }
  241. #endregion
  242. #region Constructor (client side)
  243. /// <summary>
  244. /// Create a new tcp client and connect to a remote BaseServer instance.
  245. /// </summary>
  246. /// <param name="setServerAddress">Set server address</param>
  247. /// <param name="setServerPorts">
  248. /// List of server ports we can use to connect, they will be tried in this
  249. /// order (useful in case some ports are blocked, e.g. in company networks).
  250. /// </param>
  251. /// <param name="setMaxMessageTypes">Set max number of message types,
  252. /// which is the same value as the server uses.</param>
  253. /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
  254. /// endlessly, which should only be used when you also call Dispose
  255. /// (e.g. from Application.Close or when closing the window). If this is
  256. /// done without calling Dispose and no server connect is possible, the
  257. /// connectionThread is never quit and you need to kill the process.
  258. /// </param>
  259. /// <param name="setUsername">Username for the Login method,
  260. /// can be overwritten, but this is the default value for it.</param>
  261. /// <param name="privateKey">
  262. /// Private key for encryption, see Cryptography class for details, by
  263. /// default this is null and unused. Please note that for encryption the
  264. /// first message send from the client to the server must be the Connect
  265. /// message (message type 0) and it contains the random seed (optionally
  266. /// even encrypted with a public RSA key from the server).
  267. /// Most importantly the ClientData must also have the same privateKey,
  268. /// which should be kept secret (but even if it is known by an attacked
  269. /// if you encrypted the random seed with RSA, the transmission is still
  270. /// secure).
  271. /// </param>
  272. /// <param name="publicRsaKey">
  273. /// Public RSA key, which must match the secret RSA key on the server
  274. /// (2048 bits) that is used to decrypt incoming Connect messages from
  275. /// clients, which encrypt their seed value in the Connect message.
  276. /// Note: This is ignored if privateKey is null (works only together).
  277. /// </param>
  278. protected BaseClient(string setServerAddress, int[] setServerPorts,
  279. byte setMaxMessageTypes, bool keepTryingToConnectEndlessly,
  280. string setUsername, byte[] privateKey = null,
  281. XmlNode publicRsaKey = null)
  282. {
  283. // Remember server address and port, not really used, but maybe useful
  284. // later for debugging and informational purposes.
  285. ServerAddress = setServerAddress;
  286. ServerPorts = setServerPorts;
  287. if (privateKey != null)
  288. {
  289. // Create AES encryption instance and generate a random seed.
  290. aes = new AES(privateKey);
  291. if (publicRsaKey != null)
  292. {
  293. rsa = new RSA(publicRsaKey);
  294. }
  295. }
  296. // Fill in extra information for serverConnectionAndClientInfo
  297. maxNumOfMessageTypes = setMaxMessageTypes;
  298. Username = setUsername;
  299. ConnectSocketAndStartReceivingMessages(ServerAddress, ServerPorts,
  300. keepTryingToConnectEndlessly);
  301. }
  302. /// <summary>
  303. /// Create a new tcp client and connect to a remote BaseServer instance.
  304. /// </summary>
  305. /// <param name="setServerAddress">Set server address</param>
  306. /// <param name="setServerPort">Set server port</param>
  307. /// <param name="setMaxNumOfMessageTypes">Set max number of message types,
  308. /// which is the same value as the server uses.</param>
  309. /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
  310. /// endlessly, which should only be used when you also call Dispose
  311. /// (e.g. from Application.Close or when closing the window). If this is
  312. /// done without calling Dispose and no server connect is possible, the
  313. /// connectionThread is never quit and you need to kill the process.
  314. /// </param>
  315. /// <param name="setUsername">Username for the Login method,
  316. /// can be overwritten, but this is the default value for it.</param>
  317. /// <param name="setPrivateKey">
  318. /// Private key for encryption, see Cryptography class for details, by
  319. /// default this is null and unused. Please note that for encryption the
  320. /// first message send from the client to the server must be the Connect
  321. /// message (message type 0) and it contains the random seed (optionally
  322. /// even encrypted with a public RSA key from the server).
  323. /// Most importantly the ClientData must also have the same privateKey,
  324. /// which should be kept secret (but even if it is known by an attacked
  325. /// if you encrypted the random seed with RSA, the transmission is still
  326. /// secure).
  327. /// </param>
  328. /// <param name="publicRsaKey">
  329. /// Public RSA key, which must match the secret RSA key on the server
  330. /// (2048 bits) that is used to decrypt incoming Connect messages from
  331. /// clients, which encrypt their seed value in the Connect message.
  332. /// Note: This is ignored if privateKey is null (works only together).
  333. /// </param>
  334. protected BaseClient(string setServerAddress, int setServerPort,
  335. byte setMaxMessageTypes, bool keepTryingToConnectEndlessly,
  336. string setUsername, byte[] privateKey = null,
  337. XmlNode publicRsaKey = null)
  338. : this(setServerAddress, new[]
  339. {
  340. setServerPort
  341. }, setMaxMessageTypes,
  342. keepTryingToConnectEndlessly, setUsername, privateKey, publicRsaKey)
  343. {
  344. }
  345. #endregion
  346. #endregion
  347. #region ConnectSocketAndStartReceivingMessages
  348. /// <summary>
  349. /// Helper method to connect to the server in an extra thread.
  350. /// </summary>
  351. /// <param name="serverAddress">Server address to connect to</param>
  352. /// <param name="serverPorts">Server port(s) to connect to</param>
  353. /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
  354. /// endlessly, which should only be used when you also call Dispose
  355. /// (e.g. from Application.Close or when closing the window). If this is
  356. /// done without calling Dispose and no server connect is possible, the
  357. /// connectionThread is never quit and you need to kill the process.
  358. /// </param>
  359. protected virtual void ConnectSocketAndStartReceivingMessages(
  360. string serverAddress, int[] serverPorts,
  361. bool keepTryingToConnectEndlessly)
  362. {
  363. // Do all this in an extra thread, stuff can fail and take a while!
  364. connectionThread = ThreadHelper.Start(delegate
  365. {
  366. try
  367. {
  368. if (keepTryingToConnectEndlessly)
  369. {
  370. // If we are not connected, try over and over again!
  371. while (Socket == null ||
  372. Socket.Connected == false)
  373. {
  374. // Try to connect to the server
  375. Socket = SocketHelper.ConnectTcpSocket(
  376. serverAddress, serverPorts, DefaultConnectionTimeoutMs, false);
  377. // Not connected yet? Then wait for a while (10 second delay)
  378. if (Socket == null ||
  379. Socket.Connected == false)
  380. {
  381. Thread.Sleep(WaitTimeTillNextConnectionAttemptMs);
  382. }
  383. } // while
  384. } // if
  385. else
  386. {
  387. // Try to connect to the server
  388. Socket = SocketHelper.ConnectTcpSocket(
  389. serverAddress, serverPorts, DefaultConnectionTimeoutMs, false);
  390. } // else
  391. // Done with connectionThread, it is not longer needed or used!
  392. connectionThread = null;
  393. // Get out of this thread if the connection failed
  394. if (Socket == null ||
  395. Socket.Connected == false)
  396. {
  397. Log.Info(
  398. "Unable to connect to server: " +
  399. (String.IsNullOrEmpty(SocketHelper.LastConnectionError)
  400. ? ""
  401. : SocketHelper.LastConnectionError + ": ") +
  402. serverAddress + ":" + serverPorts.Write());
  403. // (keepTryingToConnectEndlessly ? "enabled" : "disabled") + ")
  404. // Notify anyone interested that we failed to login
  405. OnLogin(null);
  406. Socket = null;
  407. return;
  408. }
  409. // We are connected, send the Connect message to setup the
  410. // connection (optionally with encryption).
  411. connected = true;
  412. StartReceivingMessages();
  413. SendConnectMessageToServer();
  414. // Also send out the Login message with all information as defined
  415. // in the SendLoginMessage and the server.OnClientLogin methods.
  416. // Note: The server also sends Login data to us, see OnLogin.
  417. SendLoginMessageToServer();
  418. // In case we tried to send out some messages earlier when the
  419. // socket was not initialized yet, do it now!
  420. lock (remToSendMessageType)
  421. {
  422. for (int i = 0; i < remToSendMessageType.Count; i++)
  423. {
  424. SocketHelper.SendMessageBytes(Socket,
  425. remToSendMessageType[i],
  426. remToSendMessageData[i],
  427. remToSendMessageCompression[i],
  428. aes);
  429. }
  430. remToSendMessageType.Clear();
  431. remToSendMessageData.Clear();
  432. remToSendMessageCompression.Clear();
  433. }
  434. } // try
  435. catch (ThreadAbortException)
  436. {
  437. // Ignore if the thread is being killed (see Dispose)!
  438. OnLogin(null);
  439. Socket = null;
  440. }
  441. catch (Exception ex)
  442. {
  443. Log.Warning("Failed to connect and login: " + ex);
  444. Dispose();
  445. }
  446. });
  447. }
  448. #endregion
  449. #region Dispose
  450. /// <summary>
  451. /// Dispose the client, will kill the threads and sockets. Also called if
  452. /// the host disconnects us and in BaseServer.OnClientDisconnects.
  453. /// </summary>
  454. public virtual void Dispose()
  455. {
  456. // Still connecting? Then kill the connection thread!
  457. if (connectionThread != null)
  458. {
  459. connectionThread.Abort();
  460. connectionThread = null;
  461. }
  462. // And finally disconnect, the thread above should have displayed
  463. // everything we sent by now. Socket connected? Then disconnect it.
  464. if (Socket != null &&
  465. Socket.Connected)
  466. {
  467. try
  468. {
  469. Socket.Shutdown(SocketShutdown.Both);
  470. Socket.Close();
  471. }
  472. catch (Exception ex)
  473. {
  474. Log.Warning("Failed to shutdown " + this + ": " + ex);
  475. }
  476. } // if
  477. // If this is on the client side and we were not connected yet, inform!
  478. if (linkToServer == null &&
  479. loggedIn == false)
  480. {
  481. OnLogin(null);
  482. }
  483. Socket = null;
  484. connected = false;
  485. }
  486. /// <summary>
  487. /// Destructor, will just call Dispose to kill the threads and sockets.
  488. /// </summary>
  489. ~BaseClient()
  490. {
  491. Dispose();
  492. }
  493. #endregion
  494. #region StartReceivingMessages
  495. /// <summary>
  496. /// Start receiving messages, which will use the SocketHelper functionality
  497. /// by default, but you can override this to get different behavior.
  498. /// All you need for that is a message received delegate, which will be
  499. /// called whenever a full message arrives to one of the clients. This
  500. /// method is all you need after you have a connected socket (either when
  501. /// you connected to a server or a server that listened for clients). By
  502. /// default messageReceived is handled by the SocketHelper automatically.
  503. /// Optionally you can also get notified whenever data arrives in case you
  504. /// want to update progress bars for big data packages.
  505. /// </summary>
  506. internal void StartReceivingMessages()
  507. {
  508. // Start the receiving process with our data and callback method!
  509. SocketHelper.SetupReceiveCallback(Socket, clientBufferData,
  510. SocketHelper.OnReceivedDataAsyncCallback, this);
  511. }
  512. #endregion
  513. #region HandleConnectEncryption
  514. /// <summary>
  515. /// Write encryption information for the Connect message, see the
  516. /// ClientData.SendConnectMessage method for details. This message starts
  517. /// with a helper byte to mark if we use encryption and then adds the
  518. /// seed value (either as a plain byte array or encrypted with RSA).
  519. /// </summary>
  520. /// <param name="writer">Writer to store binary data to</param>
  521. protected void HandleConnectEncryption(BinaryWriter writer)
  522. {
  523. // Do we use a private key for encrypting messages?
  524. // Store if just AES encryption is used (1) or if RSA is used too (2).
  525. writer.Write((byte)
  526. (aes != null
  527. ? (rsa != null
  528. ? 2
  529. : 1)
  530. : 0));
  531. if (aes != null)
  532. {
  533. // If we have no RSA enabled, just store the seed value!
  534. if (rsa == null)
  535. {
  536. writer.Write(aes.Seed);
  537. }
  538. // Otherwise encrypt the seed with our public RSA key
  539. else
  540. {
  541. writer.Write(rsa.Encrypt(aes.Seed));
  542. }
  543. }
  544. }
  545. #endregion
  546. #region SendConnectMessageToServer
  547. /// <summary>
  548. /// Send out the Connect message from the client to the server with the
  549. /// optional encryption seed value (which might even be RSA encrypted) for
  550. /// secure network transmissions. This information must always be send via
  551. /// the <see cref="HandleConnectEncryption"/> method. Additional user
  552. /// information (username, password, etc.) can also be send with help of
  553. /// the Login message (see <see cref="SendLoginMessageToServer"/>).
  554. /// <para />
  555. /// Called by <see cref="ConnectSocketAndStartReceivingMessages"/>
  556. /// </summary>
  557. private void SendConnectMessageToServer()
  558. {
  559. MemoryStream connectMessage = new MemoryStream();
  560. BinaryWriter writer = new BinaryWriter(connectMessage);
  561. // Handle encryption Connect message logic.
  562. HandleConnectEncryption(writer);
  563. // And send the Connect message
  564. Send((byte)BasicMessageTypes.Connect, connectMessage);
  565. }
  566. #endregion
  567. #region SendLoginMessageToServer
  568. /// <summary>
  569. /// Send out the Login message from the client to the server (after the
  570. /// Connect message has been set and everything is setup). By default only
  571. /// the username is sent, override this method for more functionality.
  572. /// Note: There are two reasons why Connect and Login is separated. First
  573. /// it makes deriving this method easier as we do not need to handle the
  574. /// Connect and encryption logic here. And it also makes sending,
  575. /// encrypting and receiving and decrypting the Login message easier and
  576. /// more secure (all data in Login is already encrypted and cannot be
  577. /// decrypted without a successful Connect message was processed first).
  578. /// <para />
  579. /// Called by <see cref="ConnectSocketAndStartReceivingMessages"/>
  580. /// </summary>
  581. protected virtual void SendLoginMessageToServer()
  582. {
  583. MemoryStream loginMessage = new MemoryStream();
  584. BinaryWriter writer = new BinaryWriter(loginMessage);
  585. // Write out the username (derived classes might send more)
  586. writer.Write(Username);
  587. // And send the Login message
  588. Send((byte)BasicMessageTypes.Login, loginMessage);
  589. }
  590. #endregion
  591. #region OnLogin
  592. /// <summary>
  593. /// On login helper method, will be called once the connection has been
  594. /// established and we got the Login message from the server. Does nothing
  595. /// here, but derived classes can extend this functionality to receive the
  596. /// login status, username and extra login information from the server.
  597. /// </summary>
  598. /// <param name="data">Data we received from the network</param>
  599. /// <returns>True if we are still connected and are successfully logged in
  600. /// or false otherwise if the server rejected us and we need to reconnect.
  601. /// </returns>
  602. protected virtual bool OnLogin(BinaryReader data)
  603. {
  604. // Nothing else here, but derived classes can extend this functionality
  605. return data != null;
  606. }
  607. #endregion
  608. #region OnMessageReceived
  609. /// <summary>
  610. /// When a message arrived from the server and has been processed, this
  611. /// method will be called from OnRawMessageReceived (if everything is ok).
  612. /// Override for more functionality, this method will however not report
  613. /// the Connect (0) and Login (1) BasicMessageTypes functionality and
  614. /// encryption and decryption is done automatically in the caller for us.
  615. /// <para />
  616. /// Note: This is the only method here you must implement in derived
  617. /// classes to make sense of client server communication. It is however
  618. /// not used if this is a server side client connection (see first
  619. /// constructor).
  620. /// </summary>
  621. /// <param name="messageType">Type of message received (byte)</param>
  622. /// <param name="data">Data we received from the network (uncompressed
  623. /// and un-encrypted, all handled by OnRawMessageReceived)</param>
  624. protected abstract void OnMessageReceived(byte messageType,
  625. BinaryReader data);
  626. #endregion
  627. #region HandleConnectDecryption
  628. /// <summary>
  629. /// Helper method to decrypt incoming Connect messages from clients in
  630. /// case they are encrypted (done on the server side). Will only work if
  631. /// both the client and the server use the same AES cryptography private
  632. /// keys and if used matching secret and public RSA keys for the AES seed
  633. /// value. If no cryptography is used just a byte with the value 0 is read
  634. /// from the data stream and the OnConnected method will handle all the
  635. /// rest (Username, etc.)
  636. /// <para />
  637. /// This method will disconnect the client right away if anything is wrong
  638. /// like different encryption modes on the server or client.
  639. /// Note: Called only from <see cref="OnRawMessageReceived"/> for servers.
  640. /// </summary>
  641. /// <param name="data">Data we received from the network</param>
  642. /// <returns>True if we could connect successfully, false otherwise
  643. /// (invalid connection attempt, encryption data invalid, etc.)</returns>
  644. protected bool HandleConnectDecryption(BinaryReader data)
  645. {
  646. if (linkToServer == null)
  647. {
  648. Log.Warning(
  649. "This method should only be called on the server side for " +
  650. "connected clients!");
  651. return false;
  652. }
  653. byte encryptionMode = data.ReadByte();
  654. if (encryptionMode >= 1)
  655. {
  656. // Make sure AES is also enabled on the server!
  657. if (linkToServer.privateKey == null)
  658. {
  659. Log.Warning(
  660. "The client sent a Connect request with encryption enabled, but " +
  661. "the server has no encryption setup. Will disconnect the client " +
  662. "now because he is not allowed to connect: " + this);
  663. Dispose();
  664. return false;
  665. }
  666. // Get AES instance from the server and duplicate it with the given
  667. // seed value.
  668. byte[] seed = new byte[AES.SeedLength];
  669. try
  670. {
  671. // Just read the seed value and set it.
  672. if (encryptionMode == 1)
  673. {
  674. data.Read(seed, 0, seed.Length);
  675. }
  676. else
  677. {
  678. // Make sure RSA is also enabled on the server!
  679. if (linkToServer.rsa == null)
  680. {
  681. Log.Warning(
  682. "The client sent an RSA encrypted (mode " +
  683. encryptionMode + ") Connect request, but the server has no " +
  684. "RSA decryption setup. Will disconnect the client now " +
  685. "because we cannot decrypt the AES random seed value, " +
  686. "which is needed: " + this);
  687. Dispose();
  688. return false;
  689. }
  690. // Otherwise the seed is encrypted with the public RSA key, we need
  691. // the server secret RSA key to decrypt it.
  692. byte[] rsaData = new byte[RSA.DataLength];
  693. data.Read(rsaData, 0, rsaData.Length);
  694. seed = linkToServer.rsa.Decrypt(rsaData);
  695. }
  696. aes = new AES(linkToServer.privateKey, seed);
  697. }
  698. catch (Exception ex)
  699. {
  700. // If anything does not fit here (server has no encryption set up
  701. // or any of the keys do not match or work), abort.
  702. Log.Warning(
  703. "Failed to initialize encryption for client " + this +
  704. " (will disconnect client now): " + ex);
  705. Dispose();
  706. return false;
  707. }
  708. }
  709. else
  710. {
  711. // No encryption is wanted, check if the server also has no
  712. // privateKey and no RSA encryption setup. Otherwise abort too!
  713. if (linkToServer.privateKey != null ||
  714. linkToServer.rsa != null)
  715. {
  716. Log.Warning(
  717. "Encryption is setup on the server, but the client " +
  718. this + " connected without any encryption enabled! Will " +
  719. "disconnect the client now because he is not allowed to connect.");
  720. Dispose();
  721. return false;
  722. }
  723. }
  724. // Otherwise everything went alright, we are connected now!
  725. return true;
  726. }
  727. #endregion
  728. #region OnRawMessageReceived
  729. /// <summary>
  730. /// When a message arrived from the server this method will be called from
  731. /// <see cref="SocketHelper.ReceiveMessageData"/> with the raw message type
  732. /// and data. The data can be compressed and encrypted, this method will
  733. /// handle all the decryption and decompression automatically for us and
  734. /// also handles all the Connect (0) and Login (1) functionality.
  735. /// <para />
  736. /// Note: This method does all the heavy lifting including Connection and
  737. /// Login logic plus decompression and decryption of network data. Read
  738. /// it carefully if you want to understand how the underlying message
  739. /// system works. See the Delta.Utilities.Tests for lots of examples.
  740. /// </summary>
  741. /// <param name="messageType">Type of message received (byte)</param>
  742. /// <param name="data">Data we received from the network (can be
  743. /// encrypted and compressed, see the Send methods for encryption)</param>
  744. /// <param name="isCompressed">Is the incoming data compressed and need
  745. /// to be decompressed?</param>
  746. internal void OnRawMessageReceived(byte messageType, BinaryReader data,
  747. bool isCompressed)
  748. {
  749. // Only allow Connect and Login messages initially on the server side.
  750. if (messageType == (byte)BasicMessageTypes.Connect)
  751. {
  752. if (linkToServer != null)
  753. {
  754. // Already connected? Then this message is not allowed!
  755. if (connected)
  756. {
  757. Log.Warning(
  758. "Client is already connected, it makes no sense to " +
  759. "make another Connect request (resending encryption seed " +
  760. "is currently not supported)! Please check your send code.");
  761. // Just ignore this message then
  762. return;
  763. }
  764. // Handle decryption if needed (decompression is not needed here).
  765. connected = HandleConnectDecryption(data);
  766. // Usually the client is either fully connected or has been
  767. // disconnected already (but he might still be connected if the
  768. // server allows to re-send Connect messages upon Login failure).
  769. // In any case any other message is only allowed when fully
  770. // connected logged in (see connectedAndLoggedIn and below).
  771. return;
  772. }
  773. else
  774. {
  775. Log.Warning(
  776. "Invalid Connect message received for client side " +
  777. "client! This message is only allowed on the server side! " +
  778. "Disconnecting this client now: " + this);
  779. Dispose();
  780. return;
  781. }
  782. } // if (messageType == Connect)
  783. // If we are not connected and this is anything but the Connect message
  784. // type, disconnect immediately!
  785. if (connected == false)
  786. {
  787. Log.Warning(
  788. "Invalid message (type=" + messageType + ") received " +
  789. "for this not yet connected client (only Connect is allowed until " +
  790. "the client is fully connected)! Client will be disconnected now: " +
  791. this);
  792. Dispose();
  793. return;
  794. }
  795. // Otherwise everything is fine and dandy, handle decryption (if enabled)
  796. // and decompression (only for big messages and if enabled by Send) now.
  797. // All methods below this only accept uncompressed and un-encrypted data
  798. if (aes != null &&
  799. // And we must have some data left, else this could be an empty message
  800. data.BaseStream.Position < data.BaseStream.Length)
  801. {
  802. try
  803. {
  804. // Get the original data length, might be smaller than data.Length
  805. int originalDataLength =
  806. StreamHelper.ReadNumberMostlySmallerThan254(data);
  807. // And copy all data into a byte array for decryption
  808. byte[] encryptedData =
  809. new byte[data.BaseStream.Length - data.BaseStream.Position];
  810. data.Read(encryptedData, 0, encryptedData.Length);
  811. // Decrypt all the data with our AES cryptography functionality
  812. byte[] decryptedData = aes.Decrypt(encryptedData,
  813. originalDataLength);
  814. if (isCompressed)
  815. {
  816. decryptedData = Zip.Decompress(decryptedData);
  817. }
  818. // Provide a new reader to read the data back
  819. data = new BinaryReader(new MemoryStream(decryptedData));
  820. }
  821. catch (Exception ex)
  822. {
  823. Log.Warning(
  824. "Decrypting data failed, message type=" + messageType +
  825. ", data length=" +
  826. (data != null
  827. ? data.BaseStream.Length
  828. : 0) +
  829. ", we have to ignore this message data. Error: " + ex.Message +
  830. (ex.Message.Contains("Padding is invalid and cannot be removed")
  831. ? " This means the incoming encrypted data has a wrong offset " +
  832. "and cannot be decrypted, either something bad happened at the " +
  833. "sender or something else interrupted or changed the " +
  834. "transmission (is the data even encrypted?)!"
  835. : "") +
  836. (ex.Message.Contains("The input data is not a complete block")
  837. ? " This means the incoming data is most likely not even " +
  838. "encrypted and cannot be decrypted, something bad or wrong " +
  839. "happened at sender!"
  840. : ""));
  841. return;
  842. }
  843. } // if (AES encrypted)
  844. else if (isCompressed)
  845. {
  846. // If the data was just compressed, we can just decompress it and
  847. // provide a new reader with the decompressed data to the methods.
  848. try
  849. {
  850. byte[] compressedData =
  851. new byte[data.BaseStream.Length - data.BaseStream.Position];
  852. data.Read(compressedData, 0, compressedData.Length);
  853. data = new BinaryReader(new MemoryStream(
  854. Zip.Decompress(compressedData)));
  855. }
  856. catch (Exception ex)
  857. {
  858. Log.Warning(
  859. "Decompressing data failed, the data was probably not " +
  860. "correctly compressed or is corrupted, message type=" +
  861. messageType + ", data length=" +
  862. (data != null
  863. ? data.BaseStream.Length
  864. : 0) +
  865. ", we have to ignore this message data. Error: " + ex.Message);
  866. return;
  867. }
  868. } // else if (isCompressed)
  869. // Next step is to handle the login message (both on client and server)
  870. if (messageType == (byte)BasicMessageTypes.Login)
  871. {
  872. // Are we on the server side?
  873. if (linkToServer != null)
  874. {
  875. // And try to Login client with the Login message
  876. loggedIn = linkToServer.HandleClientLogin(this, data);
  877. return;
  878. }
  879. // Only allow Login messages initially on the client side.
  880. else
  881. {
  882. // Assume login worked so far
  883. loggedIn = true;
  884. // The implementation class can decide differently however.
  885. loggedIn = OnLogin(data);
  886. return;
  887. }
  888. } // if (messageType == Login)
  889. // Before we can continue check first if the client is logged in!
  890. if (loggedIn == false)
  891. {
  892. Log.Warning(
  893. "Invalid message " + messageType + " received for this " +
  894. "client, which was not logged in by the server successfully. " +
  895. "Login must have been send and the server must accept this " +
  896. "client! Client will be disconnected now: " + this);
  897. Dispose();
  898. return;
  899. }
  900. // Okay, call OnMessageReceived for clients and use the
  901. // Server.OnMessageReceived for server side clients.
  902. if (linkToServer != null)
  903. {
  904. linkToServer.OnMessageReceived(this, messageType, data);
  905. }
  906. else
  907. {
  908. OnMessageReceived(messageType, data);
  909. }
  910. }
  911. #endregion
  912. #region HandlePreCheckMessage
  913. /// <summary>
  914. /// Handle pre check message
  915. /// </summary>
  916. /// <param name="messageType">Message type</param>
  917. /// <param name="percentageComplete">Percentage complete</param>
  918. internal void HandlePreCheckMessage(byte messageType,
  919. float percentageComplete)
  920. {
  921. OnPreCheckMessage(messageType, percentageComplete);
  922. }
  923. #endregion
  924. #region OnPreCheckMessage
  925. /// <summary>
  926. /// On pre check message is fired when a message is incoming, but not
  927. /// complete yet (should only happen for big messages like file
  928. /// transfers). This way we can update status bars and show percentages.
  929. /// Not used by default, override to do something with this information.
  930. /// <para />
  931. /// Please note that this message does not actually read or provide any
  932. /// of the payload data, all we know is the message type and the
  933. /// percentage of the message that already has been transmitted. None of
  934. /// the decryption, decompression or data reading logic happens here
  935. /// (unlike <see cref="OnRawMessageReceived"/>). If you want to provide
  936. /// additional meta data like a file size or how many MB have been
  937. /// transfered, please just send an extra message before this big one to
  938. /// show this information to the user (TotalMB * percentageComplete).
  939. /// </summary>
  940. /// <param name="messageType">Type of message we are currently receiving
  941. /// (as a byte)</param>
  942. /// <param name="percentageComplete">Percentage complete (0.0-1.0)</param>
  943. protected virtual void OnPreCheckMessage(byte messageType,
  944. float percentageComplete)
  945. {
  946. }
  947. #endregion
  948. #region Send
  949. /// <summary>
  950. /// Send an empty message with just a message type to the server.
  951. /// </summary>
  952. /// <param name="messageType">Message Type</param>
  953. public void Send(byte messageType)
  954. {
  955. SendBytes(messageType, null);
  956. }
  957. /// <summary>
  958. /// Send a message with a message type and some data in a MemoryStream
  959. /// to the server.
  960. /// </summary>
  961. /// <param name="messageType">Message Type</param>
  962. /// <param name="data">Data for this message</param>
  963. public void Send(byte messageType, MemoryStream data)
  964. {
  965. SendBytes(messageType,
  966. data != null
  967. ? data.ToArray()
  968. : null);
  969. }
  970. /// <summary>
  971. /// Send a message with a message type and a string as data to the server.
  972. /// </summary>
  973. /// <param name="messageType">Message Type</param>
  974. /// <param name="data">Data as a string, will be converted to bytes</param>
  975. public void Send(byte messageType, string data)
  976. {
  977. SendBytes(messageType, StringHelper.StringToBytes(data));
  978. }
  979. /// <summary>
  980. /// Send a message with a message type and a float as data to the server.
  981. /// </summary>
  982. /// <param name="messageType">Message Type</param>
  983. /// <param name="data">Data as float, will be converted to bytes</param>
  984. public void Send(byte messageType, float data)
  985. {
  986. MemoryStream memStream = new MemoryStream();
  987. BinaryWriter writer = new BinaryWriter(memStream);
  988. writer.Write(data);
  989. SendBytes(messageType, memStream.ToArray());
  990. }
  991. #endregion
  992. #region SendBytes
  993. /// <summary>
  994. /// Send the byte array to the server with the specified message type.
  995. /// Note: Can be overwritten to catch all sends and do some extra logic
  996. /// like collecting statistics.
  997. /// </summary>
  998. /// <param name="messageType">Type of message (as byte)</param>
  999. /// <param name="data">Data</param>
  1000. /// <param name="allowToCompressMessage">Allow to automatically compress
  1001. /// the message payload data if it is above 1024 bytes (otherwise nothing
  1002. /// will happen), on by default. Please note an extra byte with 0xFF (255)
  1003. /// is sent to mark that the incoming message is compressed.</param>
  1004. protected virtual void SendBytes(byte messageType, byte[] data,
  1005. bool allowToCompressMessage = true)
  1006. {
  1007. if (connectionThread != null)
  1008. {
  1009. // Sending is done at the end of the connectionThread
  1010. if (remToSendMessageType.Count > 100)
  1011. {
  1012. Log.Warning(
  1013. "Unable to remember more messages for client '" + this + "'. " +
  1014. "After remembering 100 network messages we should be connected " +
  1015. "now. Please make sure to check if you are connected before " +
  1016. "sending so much data. Message type=" + messageType);
  1017. }
  1018. else
  1019. {
  1020. lock (remToSendMessageType)
  1021. {
  1022. remToSendMessageType.Add(messageType);
  1023. remToSendMessageData.Add(data);
  1024. remToSendMessageCompression.Add(allowToCompressMessage);
  1025. }
  1026. }
  1027. }
  1028. else if (Socket != null)
  1029. {
  1030. // Normal sending (with compression on by default and encryption if
  1031. // it has been setup for this client instance).
  1032. SocketHelper.SendMessageBytes(Socket, messageType,
  1033. data, allowToCompressMessage, aes);
  1034. }
  1035. }
  1036. #endregion
  1037. #region SendText
  1038. /// <summary>
  1039. /// Send text message with the predefined TextMessage type (type=2).
  1040. /// </summary>
  1041. /// <param name="textMessage">Text message</param>
  1042. public void SendTextMessage(string textMessage)
  1043. {
  1044. // Only send if we have a connectionThread or Socket!
  1045. if (connectionThread != null ||
  1046. Socket != null)
  1047. {
  1048. SendBytes((byte)BasicMessageTypes.TextMessage,
  1049. StringHelper.StringToBytes(textMessage));
  1050. }
  1051. }
  1052. #endregion
  1053. #region ToString
  1054. /// <summary>
  1055. /// To string method to display some debug information about this client.
  1056. /// </summary>
  1057. /// <returns>String with detailed client and socket information</returns>
  1058. public override string ToString()
  1059. {
  1060. return "Client " +
  1061. (String.IsNullOrEmpty(ServerAddress) == false
  1062. ? "Server=" + ServerAddress +
  1063. ":" + ServerPorts.Write() + ", "
  1064. : "") +
  1065. "Username=" + Username + ", Socket=" +
  1066. SocketHelper.WriteClientInfo(Socket);
  1067. }
  1068. #endregion
  1069. }
  1070. }