/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
- // Helper to figure out networking issues, will write into console only!
- //#define LOG_STUFF
-
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Net.Sockets;
- using System.Threading;
- using Delta.Utilities.Compression;
- using Delta.Utilities.Cryptography;
- using Delta.Utilities.Helpers;
- using Delta.Utilities.Xml;
-
- namespace Delta.Utilities.Networking
- {
- /// <summary>
- /// Base class for connected clients. Contains most importantly the
- /// connected socket, but also some extra information like the username.
- /// Used both on the BaseServer side (then we just care about the socket
- /// and the basic data and networking functionality) and in the Client
- /// that connects to the BaseServer (then all features are used). Derive
- /// from this to add more functionality. If you just want to use this
- /// class the MessageReceived is the easiest way to receive network data.
- /// <para />
- /// This class also support encryption with AES and RSA to make network
- /// transmissions really secure. See the constructors for details.
- /// <para />
- /// Internally also used as a receiving data helper class by defining
- /// buffers for receiving data. Thanks to the IAsyncState passed around in
- /// StartReceivingMessages this data will be preserved as long as the client
- /// is connected. RememberLastData is important for partial message we might
- /// receive (if incoming data is bigger than receivedData can hold or if we
- /// get multiple messages at once or strange packet sizes, which is very
- /// normal for TCP packets).
- /// </summary>
- public abstract class BaseClient : IDisposable
- {
- #region Constants
- /// <summary>
- /// Number of basic message types (add this if you are planing to use
- /// the BasicMessageTypes plus own types in derived classes).
- /// </summary>
- public static readonly byte NumberOfBasicMessageTypes =
- (byte)EnumHelper.GetCount<BasicMessageTypes>();
-
- /// <summary>
- /// Default time to wait until we timeout a connection attempt. Since this
- /// runs in an extra thread we do not actually have to wait this long.
- /// The default wait time is 3 seconds, which should be enough for most
- /// users (even if a timeout happens, the user can try again).
- /// </summary>
- private const int DefaultConnectionTimeoutMs = 3000;
-
- /// <summary>
- /// Wait time in ms until we try to connect again if the connection failed.
- /// We wait for 1 seconds (3 in release mode) and then try again.
- /// More is too annoying when developing and this happens (because we
- /// might just started the server or just fixed our connection problems).
- /// </summary>
- private const int WaitTimeTillNextConnectionAttemptMs =
- 3000;
- #endregion
-
- #region Public
- /// <summary>
- /// Username for this client. We mostly only care on the server what the
- /// name of each connected client is (for identification and debugging).
- /// This is set after receiving the Connect message from the client in
- /// the OnConnected method and used quite a bit on servers and clients.
- /// More complex clients with username, passwords, emails, etc. obviously
- /// have more data and you don't have to use this at all (just override
- /// the Login methods).
- /// </summary>
- public string Username;
-
- /// <summary>
- /// Client socket if we are the server. If we are the client this is the
- /// server connection socket we send all data to.
- /// </summary>
- public Socket Socket
- {
- get;
- protected internal set;
- }
-
- /// <summary>
- /// Helper method to check if we are successfully logged in. Please do not
- /// use this one to check if we have received the login message because
- /// this will not be true if the server rejected us and disconnects us.
- /// Instead override the OnLogin method to see when we are connected or
- /// when we might be disconnected before this ever gets true.
- /// </summary>
- public bool SuccessfullyLoggedIn
- {
- get
- {
- return connected &&
- loggedIn;
- }
- }
-
- /// <summary>
- /// Address to the server. Only set in constructor, cannot be changed.
- /// Not used for BaseServer clients.
- /// </summary>
- protected string ServerAddress
- {
- get;
- private set;
- }
-
- /// <summary>
- /// List of server ports we can use to connect, they will be tried in this
- /// order (useful in case some ports are blocked, e.g. in company networks).
- /// Only set in constructor, cannot be changed. Not used for BaseServer
- /// clients.
- /// </summary>
- protected int[] ServerPorts
- {
- get;
- private set;
- }
-
- /// <summary>
- /// Is the connection made on the client side or is this just the
- /// connection from the server to the client (then this returns false).
- /// </summary>
- public bool IsClientSide
- {
- get
- {
- return linkToServer == null;
- }
- }
-
- #region OnDisconnected event
- /// <summary>
- /// Delegate for the BaseClient.OnDisconnected event.
- /// </summary>
- public delegate void DisconnectDelegate();
-
- /// <summary>
- /// Delegate to be notified when we lose the connection to the server.
- /// Note: This is only called when the server disconnects us (or we lose
- /// the network connection somehow), this is not called when we dispose
- /// the BaseClient via the Dispose method (which you can override if you
- /// want to handle normal disposes).
- /// </summary>
- public DisconnectDelegate OnDisconnected;
- #endregion
-
- #endregion
-
- #region Private
- /// <summary>
- /// Helper flag to remember if we have successfully connected (for server
- /// side client connections). Initially this is false and only after
- /// receiving the Connect (on the server) message with success we are
- /// fully connected (or once we established the connection on the client
- /// side). When not connected any attempt to receive any other message will
- /// result in failure and the client will immediately disconnect in the
- /// <see cref="OnRawMessageReceived"/> method.
- /// </summary>
- private bool connected;
-
- /// <summary>
- /// Is the client logged in? Initially false and will be set to true if
- /// the Login message has been received and is successful (both on the
- /// client and the server side). Any attempt to receive or send messages
- /// to a non connected and not loggedIn client will immediately result in
- /// a failure and the client will be disconnected.
- /// </summary>
- protected bool loggedIn;
-
- /// <summary>
- /// Maximum number of message types, set in constructor. Only used to
- /// provide checks if incoming data is valid and can be converted into
- /// a known message type with data.
- /// </summary>
- internal byte maxNumOfMessageTypes;
-
- /// <summary>
- /// Client buffer data, internal to allow SocketHelper access to this
- /// and protected to allow derived classes to override the
- /// StartReceivingMessagesAndSendConnectMessage method.
- /// </summary>
- protected internal byte[] clientBufferData =
- new byte[SocketHelper.ReceiveBufferSize];
-
- /// <summary>
- /// Remember last data, also only used for the SocketHelper class.
- /// </summary>
- internal byte[] rememberLastData;
-
- /// <summary>
- /// Helper thread to make connection initialization go as smoothly as
- /// possible. Note: This is only used on the client side (server already
- /// has a connected socket when creating a new Client instance).
- /// If we are still connecting sending out messages is delayed. If the
- /// connection is already established, we can send out messages directly
- /// (which is easier and quicker and this thread is not longer needed).
- /// </summary>
- private Thread connectionThread;
-
- /// <summary>
- /// Remember AES cryptography if this is wanted for the client server
- /// communication. Both the client and the server instance must be
- /// initialized with the same private AES key. The server has to wait
- /// until the Connected message arrives from the client to setup the
- /// Cryptography class with this same private key and seed value.
- /// </summary>
- private AES aes;
-
- /// <summary>
- /// You can even use an public RSA key to encrypt Connect messages, which
- /// can be decrypted the matching secret RSA key on the server. If this
- /// variable is null, no RSA cryptography is used.
- /// </summary>
- private readonly RSA rsa;
-
- /// <summary>
- /// Link to the BaseServer if this is a connected client on the server.
- /// Needed to check if cryptography data is valid and to decrypt it.
- /// </summary>
- private readonly BaseServer linkToServer;
-
- /// <summary>
- /// Helper list to send out messages in case we are not connected yet!
- /// </summary>
- private readonly List<byte> remToSendMessageType = new List<byte>();
-
- /// <summary>
- /// Helper list for the data of messages to be send out.
- /// </summary>
- private readonly List<byte[]> remToSendMessageData = new List<byte[]>();
-
- /// <summary>
- /// Helper list for the data of messages to be send out if compression
- /// should be enabled or not (default is true).
- /// </summary>
- private readonly List<bool> remToSendMessageCompression = new List<bool>();
- #endregion
-
- #region Constructors
-
- #region Constructor (server side)
- /// <summary>
- /// Create client instance with an already known connected socket. Only
- /// used on the server side for client connections. The client side has to
- /// use the other constructor to get connected to the server.
- /// This constructor will cause the ClientSide property to return false.
- /// </summary>
- /// <param name="setMaxNumOfMessageTypes">Set max number of message types,
- /// which is the same value as the server uses.</param>
- /// <param name="setClientSocket">Get client socket</param>
- /// <param name="setLinkToServer">
- /// Link to the BaseServer if this is a connected client on the server.
- /// Needed to check if cryptography data is valid and to decrypt.
- /// </param>
- protected BaseClient(Socket setClientSocket, byte setMaxNumOfMessageTypes,
- BaseServer setLinkToServer)
- {
- Socket = setClientSocket;
- maxNumOfMessageTypes = setMaxNumOfMessageTypes;
- linkToServer = setLinkToServer;
- // Everything else is either not used or set later.
- }
- #endregion
-
- #region Constructor (client side)
- /// <summary>
- /// Create a new tcp client and connect to a remote BaseServer instance.
- /// </summary>
- /// <param name="setServerAddress">Set server address</param>
- /// <param name="setServerPorts">
- /// List of server ports we can use to connect, they will be tried in this
- /// order (useful in case some ports are blocked, e.g. in company networks).
- /// </param>
- /// <param name="setMaxMessageTypes">Set max number of message types,
- /// which is the same value as the server uses.</param>
- /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
- /// endlessly, which should only be used when you also call Dispose
- /// (e.g. from Application.Close or when closing the window). If this is
- /// done without calling Dispose and no server connect is possible, the
- /// connectionThread is never quit and you need to kill the process.
- /// </param>
- /// <param name="setUsername">Username for the Login method,
- /// can be overwritten, but this is the default value for it.</param>
- /// <param name="privateKey">
- /// Private key for encryption, see Cryptography class for details, by
- /// default this is null and unused. Please note that for encryption the
- /// first message send from the client to the server must be the Connect
- /// message (message type 0) and it contains the random seed (optionally
- /// even encrypted with a public RSA key from the server).
- /// Most importantly the ClientData must also have the same privateKey,
- /// which should be kept secret (but even if it is known by an attacked
- /// if you encrypted the random seed with RSA, the transmission is still
- /// secure).
- /// </param>
- /// <param name="publicRsaKey">
- /// Public RSA key, which must match the secret RSA key on the server
- /// (2048 bits) that is used to decrypt incoming Connect messages from
- /// clients, which encrypt their seed value in the Connect message.
- /// Note: This is ignored if privateKey is null (works only together).
- /// </param>
- protected BaseClient(string setServerAddress, int[] setServerPorts,
- byte setMaxMessageTypes, bool keepTryingToConnectEndlessly,
- string setUsername, byte[] privateKey = null,
- XmlNode publicRsaKey = null)
- {
- // Remember server address and port, not really used, but maybe useful
- // later for debugging and informational purposes.
- ServerAddress = setServerAddress;
- ServerPorts = setServerPorts;
- if (privateKey != null)
- {
- // Create AES encryption instance and generate a random seed.
- aes = new AES(privateKey);
- if (publicRsaKey != null)
- {
- rsa = new RSA(publicRsaKey);
- }
- }
-
- // Fill in extra information for serverConnectionAndClientInfo
- maxNumOfMessageTypes = setMaxMessageTypes;
- Username = setUsername;
- ConnectSocketAndStartReceivingMessages(ServerAddress, ServerPorts,
- keepTryingToConnectEndlessly);
- }
-
- /// <summary>
- /// Create a new tcp client and connect to a remote BaseServer instance.
- /// </summary>
- /// <param name="setServerAddress">Set server address</param>
- /// <param name="setServerPort">Set server port</param>
- /// <param name="setMaxNumOfMessageTypes">Set max number of message types,
- /// which is the same value as the server uses.</param>
- /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
- /// endlessly, which should only be used when you also call Dispose
- /// (e.g. from Application.Close or when closing the window). If this is
- /// done without calling Dispose and no server connect is possible, the
- /// connectionThread is never quit and you need to kill the process.
- /// </param>
- /// <param name="setUsername">Username for the Login method,
- /// can be overwritten, but this is the default value for it.</param>
- /// <param name="setPrivateKey">
- /// Private key for encryption, see Cryptography class for details, by
- /// default this is null and unused. Please note that for encryption the
- /// first message send from the client to the server must be the Connect
- /// message (message type 0) and it contains the random seed (optionally
- /// even encrypted with a public RSA key from the server).
- /// Most importantly the ClientData must also have the same privateKey,
- /// which should be kept secret (but even if it is known by an attacked
- /// if you encrypted the random seed with RSA, the transmission is still
- /// secure).
- /// </param>
- /// <param name="publicRsaKey">
- /// Public RSA key, which must match the secret RSA key on the server
- /// (2048 bits) that is used to decrypt incoming Connect messages from
- /// clients, which encrypt their seed value in the Connect message.
- /// Note: This is ignored if privateKey is null (works only together).
- /// </param>
- protected BaseClient(string setServerAddress, int setServerPort,
- byte setMaxMessageTypes, bool keepTryingToConnectEndlessly,
- string setUsername, byte[] privateKey = null,
- XmlNode publicRsaKey = null)
- : this(setServerAddress, new[]
- {
- setServerPort
- }, setMaxMessageTypes,
- keepTryingToConnectEndlessly, setUsername, privateKey, publicRsaKey)
- {
- }
- #endregion
-
- #endregion
-
- #region ConnectSocketAndStartReceivingMessages
- /// <summary>
- /// Helper method to connect to the server in an extra thread.
- /// </summary>
- /// <param name="serverAddress">Server address to connect to</param>
- /// <param name="serverPorts">Server port(s) to connect to</param>
- /// <param name="keepTryingToConnectEndlessly">Keep trying to connect
- /// endlessly, which should only be used when you also call Dispose
- /// (e.g. from Application.Close or when closing the window). If this is
- /// done without calling Dispose and no server connect is possible, the
- /// connectionThread is never quit and you need to kill the process.
- /// </param>
- protected virtual void ConnectSocketAndStartReceivingMessages(
- string serverAddress, int[] serverPorts,
- bool keepTryingToConnectEndlessly)
- {
- // Do all this in an extra thread, stuff can fail and take a while!
- connectionThread = ThreadHelper.Start(delegate
- {
- try
- {
- if (keepTryingToConnectEndlessly)
- {
- // If we are not connected, try over and over again!
- while (Socket == null ||
- Socket.Connected == false)
- {
- // Try to connect to the server
- Socket = SocketHelper.ConnectTcpSocket(
- serverAddress, serverPorts, DefaultConnectionTimeoutMs, false);
- // Not connected yet? Then wait for a while (10 second delay)
- if (Socket == null ||
- Socket.Connected == false)
- {
- Thread.Sleep(WaitTimeTillNextConnectionAttemptMs);
- }
- } // while
- } // if
- else
- {
- // Try to connect to the server
- Socket = SocketHelper.ConnectTcpSocket(
- serverAddress, serverPorts, DefaultConnectionTimeoutMs, false);
- } // else
-
- // Done with connectionThread, it is not longer needed or used!
- connectionThread = null;
-
- // Get out of this thread if the connection failed
- if (Socket == null ||
- Socket.Connected == false)
- {
- Log.Info(
- "Unable to connect to server: " +
- (String.IsNullOrEmpty(SocketHelper.LastConnectionError)
- ? ""
- : SocketHelper.LastConnectionError + ": ") +
- serverAddress + ":" + serverPorts.Write());
- // (keepTryingToConnectEndlessly ? "enabled" : "disabled") + ")
- // Notify anyone interested that we failed to login
- OnLogin(null);
- Socket = null;
- return;
- }
-
- // We are connected, send the Connect message to setup the
- // connection (optionally with encryption).
- connected = true;
- StartReceivingMessages();
- SendConnectMessageToServer();
- // Also send out the Login message with all information as defined
- // in the SendLoginMessage and the server.OnClientLogin methods.
- // Note: The server also sends Login data to us, see OnLogin.
- SendLoginMessageToServer();
-
- // In case we tried to send out some messages earlier when the
- // socket was not initialized yet, do it now!
- lock (remToSendMessageType)
- {
- for (int i = 0; i < remToSendMessageType.Count; i++)
- {
- SocketHelper.SendMessageBytes(Socket,
- remToSendMessageType[i],
- remToSendMessageData[i],
- remToSendMessageCompression[i],
- aes);
- }
- remToSendMessageType.Clear();
- remToSendMessageData.Clear();
- remToSendMessageCompression.Clear();
- }
- } // try
- catch (ThreadAbortException)
- {
- // Ignore if the thread is being killed (see Dispose)!
- OnLogin(null);
- Socket = null;
- }
- catch (Exception ex)
- {
- Log.Warning("Failed to connect and login: " + ex);
- Dispose();
- }
- });
- }
- #endregion
-
- #region Dispose
- /// <summary>
- /// Dispose the client, will kill the threads and sockets. Also called if
- /// the host disconnects us and in BaseServer.OnClientDisconnects.
- /// </summary>
- public virtual void Dispose()
- {
- // Still connecting? Then kill the connection thread!
- if (connectionThread != null)
- {
- connectionThread.Abort();
- connectionThread = null;
- }
-
- // And finally disconnect, the thread above should have displayed
- // everything we sent by now. Socket connected? Then disconnect it.
- if (Socket != null &&
- Socket.Connected)
- {
- try
- {
- Socket.Shutdown(SocketShutdown.Both);
- Socket.Close();
- }
- catch (Exception ex)
- {
- Log.Warning("Failed to shutdown " + this + ": " + ex);
- }
- } // if
-
- // If this is on the client side and we were not connected yet, inform!
- if (linkToServer == null &&
- loggedIn == false)
- {
- OnLogin(null);
- }
- Socket = null;
- connected = false;
- }
-
- /// <summary>
- /// Destructor, will just call Dispose to kill the threads and sockets.
- /// </summary>
- ~BaseClient()
- {
- Dispose();
- }
- #endregion
-
- #region StartReceivingMessages
- /// <summary>
- /// Start receiving messages, which will use the SocketHelper functionality
- /// by default, but you can override this to get different behavior.
- /// All you need for that is a message received delegate, which will be
- /// called whenever a full message arrives to one of the clients. This
- /// method is all you need after you have a connected socket (either when
- /// you connected to a server or a server that listened for clients). By
- /// default messageReceived is handled by the SocketHelper automatically.
- /// Optionally you can also get notified whenever data arrives in case you
- /// want to update progress bars for big data packages.
- /// </summary>
- internal void StartReceivingMessages()
- {
- // Start the receiving process with our data and callback method!
- SocketHelper.SetupReceiveCallback(Socket, clientBufferData,
- SocketHelper.OnReceivedDataAsyncCallback, this);
- }
- #endregion
-
- #region HandleConnectEncryption
- /// <summary>
- /// Write encryption information for the Connect message, see the
- /// ClientData.SendConnectMessage method for details. This message starts
- /// with a helper byte to mark if we use encryption and then adds the
- /// seed value (either as a plain byte array or encrypted with RSA).
- /// </summary>
- /// <param name="writer">Writer to store binary data to</param>
- protected void HandleConnectEncryption(BinaryWriter writer)
- {
- // Do we use a private key for encrypting messages?
- // Store if just AES encryption is used (1) or if RSA is used too (2).
- writer.Write((byte)
- (aes != null
- ? (rsa != null
- ? 2
- : 1)
- : 0));
- if (aes != null)
- {
- // If we have no RSA enabled, just store the seed value!
- if (rsa == null)
- {
- writer.Write(aes.Seed);
- }
- // Otherwise encrypt the seed with our public RSA key
- else
- {
- writer.Write(rsa.Encrypt(aes.Seed));
- }
- }
- }
- #endregion
-
- #region SendConnectMessageToServer
- /// <summary>
- /// Send out the Connect message from the client to the server with the
- /// optional encryption seed value (which might even be RSA encrypted) for
- /// secure network transmissions. This information must always be send via
- /// the <see cref="HandleConnectEncryption"/> method. Additional user
- /// information (username, password, etc.) can also be send with help of
- /// the Login message (see <see cref="SendLoginMessageToServer"/>).
- /// <para />
- /// Called by <see cref="ConnectSocketAndStartReceivingMessages"/>
- /// </summary>
- private void SendConnectMessageToServer()
- {
- MemoryStream connectMessage = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(connectMessage);
- // Handle encryption Connect message logic.
- HandleConnectEncryption(writer);
- // And send the Connect message
- Send((byte)BasicMessageTypes.Connect, connectMessage);
- }
- #endregion
-
- #region SendLoginMessageToServer
- /// <summary>
- /// Send out the Login message from the client to the server (after the
- /// Connect message has been set and everything is setup). By default only
- /// the username is sent, override this method for more functionality.
- /// Note: There are two reasons why Connect and Login is separated. First
- /// it makes deriving this method easier as we do not need to handle the
- /// Connect and encryption logic here. And it also makes sending,
- /// encrypting and receiving and decrypting the Login message easier and
- /// more secure (all data in Login is already encrypted and cannot be
- /// decrypted without a successful Connect message was processed first).
- /// <para />
- /// Called by <see cref="ConnectSocketAndStartReceivingMessages"/>
- /// </summary>
- protected virtual void SendLoginMessageToServer()
- {
- MemoryStream loginMessage = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(loginMessage);
- // Write out the username (derived classes might send more)
- writer.Write(Username);
- // And send the Login message
- Send((byte)BasicMessageTypes.Login, loginMessage);
- }
- #endregion
-
- #region OnLogin
- /// <summary>
- /// On login helper method, will be called once the connection has been
- /// established and we got the Login message from the server. Does nothing
- /// here, but derived classes can extend this functionality to receive the
- /// login status, username and extra login information from the server.
- /// </summary>
- /// <param name="data">Data we received from the network</param>
- /// <returns>True if we are still connected and are successfully logged in
- /// or false otherwise if the server rejected us and we need to reconnect.
- /// </returns>
- protected virtual bool OnLogin(BinaryReader data)
- {
- // Nothing else here, but derived classes can extend this functionality
- return data != null;
- }
- #endregion
-
- #region OnMessageReceived
- /// <summary>
- /// When a message arrived from the server and has been processed, this
- /// method will be called from OnRawMessageReceived (if everything is ok).
- /// Override for more functionality, this method will however not report
- /// the Connect (0) and Login (1) BasicMessageTypes functionality and
- /// encryption and decryption is done automatically in the caller for us.
- /// <para />
- /// Note: This is the only method here you must implement in derived
- /// classes to make sense of client server communication. It is however
- /// not used if this is a server side client connection (see first
- /// constructor).
- /// </summary>
- /// <param name="messageType">Type of message received (byte)</param>
- /// <param name="data">Data we received from the network (uncompressed
- /// and un-encrypted, all handled by OnRawMessageReceived)</param>
- protected abstract void OnMessageReceived(byte messageType,
- BinaryReader data);
- #endregion
-
- #region HandleConnectDecryption
- /// <summary>
- /// Helper method to decrypt incoming Connect messages from clients in
- /// case they are encrypted (done on the server side). Will only work if
- /// both the client and the server use the same AES cryptography private
- /// keys and if used matching secret and public RSA keys for the AES seed
- /// value. If no cryptography is used just a byte with the value 0 is read
- /// from the data stream and the OnConnected method will handle all the
- /// rest (Username, etc.)
- /// <para />
- /// This method will disconnect the client right away if anything is wrong
- /// like different encryption modes on the server or client.
- /// Note: Called only from <see cref="OnRawMessageReceived"/> for servers.
- /// </summary>
- /// <param name="data">Data we received from the network</param>
- /// <returns>True if we could connect successfully, false otherwise
- /// (invalid connection attempt, encryption data invalid, etc.)</returns>
- protected bool HandleConnectDecryption(BinaryReader data)
- {
- if (linkToServer == null)
- {
- Log.Warning(
- "This method should only be called on the server side for " +
- "connected clients!");
- return false;
- }
-
- byte encryptionMode = data.ReadByte();
- if (encryptionMode >= 1)
- {
- // Make sure AES is also enabled on the server!
- if (linkToServer.privateKey == null)
- {
- Log.Warning(
- "The client sent a Connect request with encryption enabled, but " +
- "the server has no encryption setup. Will disconnect the client " +
- "now because he is not allowed to connect: " + this);
- Dispose();
- return false;
- }
-
- // Get AES instance from the server and duplicate it with the given
- // seed value.
- byte[] seed = new byte[AES.SeedLength];
-
- try
- {
- // Just read the seed value and set it.
- if (encryptionMode == 1)
- {
- data.Read(seed, 0, seed.Length);
- }
- else
- {
- // Make sure RSA is also enabled on the server!
- if (linkToServer.rsa == null)
- {
- Log.Warning(
- "The client sent an RSA encrypted (mode " +
- encryptionMode + ") Connect request, but the server has no " +
- "RSA decryption setup. Will disconnect the client now " +
- "because we cannot decrypt the AES random seed value, " +
- "which is needed: " + this);
- Dispose();
- return false;
- }
-
- // Otherwise the seed is encrypted with the public RSA key, we need
- // the server secret RSA key to decrypt it.
- byte[] rsaData = new byte[RSA.DataLength];
- data.Read(rsaData, 0, rsaData.Length);
- seed = linkToServer.rsa.Decrypt(rsaData);
- }
-
- aes = new AES(linkToServer.privateKey, seed);
- }
- catch (Exception ex)
- {
- // If anything does not fit here (server has no encryption set up
- // or any of the keys do not match or work), abort.
- Log.Warning(
- "Failed to initialize encryption for client " + this +
- " (will disconnect client now): " + ex);
- Dispose();
- return false;
- }
- }
- else
- {
- // No encryption is wanted, check if the server also has no
- // privateKey and no RSA encryption setup. Otherwise abort too!
- if (linkToServer.privateKey != null ||
- linkToServer.rsa != null)
- {
- Log.Warning(
- "Encryption is setup on the server, but the client " +
- this + " connected without any encryption enabled! Will " +
- "disconnect the client now because he is not allowed to connect.");
- Dispose();
- return false;
- }
- }
-
- // Otherwise everything went alright, we are connected now!
- return true;
- }
- #endregion
-
- #region OnRawMessageReceived
- /// <summary>
- /// When a message arrived from the server this method will be called from
- /// <see cref="SocketHelper.ReceiveMessageData"/> with the raw message type
- /// and data. The data can be compressed and encrypted, this method will
- /// handle all the decryption and decompression automatically for us and
- /// also handles all the Connect (0) and Login (1) functionality.
- /// <para />
- /// Note: This method does all the heavy lifting including Connection and
- /// Login logic plus decompression and decryption of network data. Read
- /// it carefully if you want to understand how the underlying message
- /// system works. See the Delta.Utilities.Tests for lots of examples.
- /// </summary>
- /// <param name="messageType">Type of message received (byte)</param>
- /// <param name="data">Data we received from the network (can be
- /// encrypted and compressed, see the Send methods for encryption)</param>
- /// <param name="isCompressed">Is the incoming data compressed and need
- /// to be decompressed?</param>
- internal void OnRawMessageReceived(byte messageType, BinaryReader data,
- bool isCompressed)
- {
-
- // Only allow Connect and Login messages initially on the server side.
- if (messageType == (byte)BasicMessageTypes.Connect)
- {
- if (linkToServer != null)
- {
- // Already connected? Then this message is not allowed!
- if (connected)
- {
- Log.Warning(
- "Client is already connected, it makes no sense to " +
- "make another Connect request (resending encryption seed " +
- "is currently not supported)! Please check your send code.");
- // Just ignore this message then
- return;
- }
-
- // Handle decryption if needed (decompression is not needed here).
- connected = HandleConnectDecryption(data);
- // Usually the client is either fully connected or has been
- // disconnected already (but he might still be connected if the
- // server allows to re-send Connect messages upon Login failure).
- // In any case any other message is only allowed when fully
- // connected logged in (see connectedAndLoggedIn and below).
- return;
- }
- else
- {
- Log.Warning(
- "Invalid Connect message received for client side " +
- "client! This message is only allowed on the server side! " +
- "Disconnecting this client now: " + this);
- Dispose();
- return;
- }
- } // if (messageType == Connect)
-
- // If we are not connected and this is anything but the Connect message
- // type, disconnect immediately!
- if (connected == false)
- {
- Log.Warning(
- "Invalid message (type=" + messageType + ") received " +
- "for this not yet connected client (only Connect is allowed until " +
- "the client is fully connected)! Client will be disconnected now: " +
- this);
- Dispose();
- return;
- }
-
- // Otherwise everything is fine and dandy, handle decryption (if enabled)
- // and decompression (only for big messages and if enabled by Send) now.
- // All methods below this only accept uncompressed and un-encrypted data
- if (aes != null &&
- // And we must have some data left, else this could be an empty message
- data.BaseStream.Position < data.BaseStream.Length)
- {
- try
- {
- // Get the original data length, might be smaller than data.Length
- int originalDataLength =
- StreamHelper.ReadNumberMostlySmallerThan254(data);
- // And copy all data into a byte array for decryption
- byte[] encryptedData =
- new byte[data.BaseStream.Length - data.BaseStream.Position];
- data.Read(encryptedData, 0, encryptedData.Length);
- // Decrypt all the data with our AES cryptography functionality
- byte[] decryptedData = aes.Decrypt(encryptedData,
- originalDataLength);
- if (isCompressed)
- {
- decryptedData = Zip.Decompress(decryptedData);
- }
- // Provide a new reader to read the data back
- data = new BinaryReader(new MemoryStream(decryptedData));
- }
- catch (Exception ex)
- {
- Log.Warning(
- "Decrypting data failed, message type=" + messageType +
- ", data length=" +
- (data != null
- ? data.BaseStream.Length
- : 0) +
- ", we have to ignore this message data. Error: " + ex.Message +
- (ex.Message.Contains("Padding is invalid and cannot be removed")
- ? " This means the incoming encrypted data has a wrong offset " +
- "and cannot be decrypted, either something bad happened at the " +
- "sender or something else interrupted or changed the " +
- "transmission (is the data even encrypted?)!"
- : "") +
- (ex.Message.Contains("The input data is not a complete block")
- ? " This means the incoming data is most likely not even " +
- "encrypted and cannot be decrypted, something bad or wrong " +
- "happened at sender!"
- : ""));
- return;
- }
- } // if (AES encrypted)
- else if (isCompressed)
- {
- // If the data was just compressed, we can just decompress it and
- // provide a new reader with the decompressed data to the methods.
- try
- {
- byte[] compressedData =
- new byte[data.BaseStream.Length - data.BaseStream.Position];
- data.Read(compressedData, 0, compressedData.Length);
- data = new BinaryReader(new MemoryStream(
- Zip.Decompress(compressedData)));
- }
- catch (Exception ex)
- {
- Log.Warning(
- "Decompressing data failed, the data was probably not " +
- "correctly compressed or is corrupted, message type=" +
- messageType + ", data length=" +
- (data != null
- ? data.BaseStream.Length
- : 0) +
- ", we have to ignore this message data. Error: " + ex.Message);
- return;
- }
- } // else if (isCompressed)
-
- // Next step is to handle the login message (both on client and server)
- if (messageType == (byte)BasicMessageTypes.Login)
- {
- // Are we on the server side?
- if (linkToServer != null)
- {
- // And try to Login client with the Login message
- loggedIn = linkToServer.HandleClientLogin(this, data);
- return;
- }
- // Only allow Login messages initially on the client side.
- else
- {
- // Assume login worked so far
- loggedIn = true;
- // The implementation class can decide differently however.
- loggedIn = OnLogin(data);
- return;
- }
- } // if (messageType == Login)
-
- // Before we can continue check first if the client is logged in!
- if (loggedIn == false)
- {
- Log.Warning(
- "Invalid message " + messageType + " received for this " +
- "client, which was not logged in by the server successfully. " +
- "Login must have been send and the server must accept this " +
- "client! Client will be disconnected now: " + this);
- Dispose();
- return;
- }
-
- // Okay, call OnMessageReceived for clients and use the
- // Server.OnMessageReceived for server side clients.
- if (linkToServer != null)
- {
- linkToServer.OnMessageReceived(this, messageType, data);
- }
- else
- {
- OnMessageReceived(messageType, data);
- }
- }
- #endregion
-
- #region HandlePreCheckMessage
- /// <summary>
- /// Handle pre check message
- /// </summary>
- /// <param name="messageType">Message type</param>
- /// <param name="percentageComplete">Percentage complete</param>
- internal void HandlePreCheckMessage(byte messageType,
- float percentageComplete)
- {
- OnPreCheckMessage(messageType, percentageComplete);
- }
- #endregion
-
- #region OnPreCheckMessage
- /// <summary>
- /// On pre check message is fired when a message is incoming, but not
- /// complete yet (should only happen for big messages like file
- /// transfers). This way we can update status bars and show percentages.
- /// Not used by default, override to do something with this information.
- /// <para />
- /// Please note that this message does not actually read or provide any
- /// of the payload data, all we know is the message type and the
- /// percentage of the message that already has been transmitted. None of
- /// the decryption, decompression or data reading logic happens here
- /// (unlike <see cref="OnRawMessageReceived"/>). If you want to provide
- /// additional meta data like a file size or how many MB have been
- /// transfered, please just send an extra message before this big one to
- /// show this information to the user (TotalMB * percentageComplete).
- /// </summary>
- /// <param name="messageType">Type of message we are currently receiving
- /// (as a byte)</param>
- /// <param name="percentageComplete">Percentage complete (0.0-1.0)</param>
- protected virtual void OnPreCheckMessage(byte messageType,
- float percentageComplete)
- {
- }
- #endregion
-
- #region Send
- /// <summary>
- /// Send an empty message with just a message type to the server.
- /// </summary>
- /// <param name="messageType">Message Type</param>
- public void Send(byte messageType)
- {
- SendBytes(messageType, null);
- }
-
- /// <summary>
- /// Send a message with a message type and some data in a MemoryStream
- /// to the server.
- /// </summary>
- /// <param name="messageType">Message Type</param>
- /// <param name="data">Data for this message</param>
- public void Send(byte messageType, MemoryStream data)
- {
- SendBytes(messageType,
- data != null
- ? data.ToArray()
- : null);
- }
-
- /// <summary>
- /// Send a message with a message type and a string as data to the server.
- /// </summary>
- /// <param name="messageType">Message Type</param>
- /// <param name="data">Data as a string, will be converted to bytes</param>
- public void Send(byte messageType, string data)
- {
- SendBytes(messageType, StringHelper.StringToBytes(data));
- }
-
- /// <summary>
- /// Send a message with a message type and a float as data to the server.
- /// </summary>
- /// <param name="messageType">Message Type</param>
- /// <param name="data">Data as float, will be converted to bytes</param>
- public void Send(byte messageType, float data)
- {
- MemoryStream memStream = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(memStream);
- writer.Write(data);
- SendBytes(messageType, memStream.ToArray());
- }
- #endregion
-
- #region SendBytes
- /// <summary>
- /// Send the byte array to the server with the specified message type.
- /// Note: Can be overwritten to catch all sends and do some extra logic
- /// like collecting statistics.
- /// </summary>
- /// <param name="messageType">Type of message (as byte)</param>
- /// <param name="data">Data</param>
- /// <param name="allowToCompressMessage">Allow to automatically compress
- /// the message payload data if it is above 1024 bytes (otherwise nothing
- /// will happen), on by default. Please note an extra byte with 0xFF (255)
- /// is sent to mark that the incoming message is compressed.</param>
- protected virtual void SendBytes(byte messageType, byte[] data,
- bool allowToCompressMessage = true)
- {
- if (connectionThread != null)
- {
- // Sending is done at the end of the connectionThread
- if (remToSendMessageType.Count > 100)
- {
- Log.Warning(
- "Unable to remember more messages for client '" + this + "'. " +
- "After remembering 100 network messages we should be connected " +
- "now. Please make sure to check if you are connected before " +
- "sending so much data. Message type=" + messageType);
- }
- else
- {
- lock (remToSendMessageType)
- {
- remToSendMessageType.Add(messageType);
- remToSendMessageData.Add(data);
- remToSendMessageCompression.Add(allowToCompressMessage);
- }
- }
- }
- else if (Socket != null)
- {
- // Normal sending (with compression on by default and encryption if
- // it has been setup for this client instance).
- SocketHelper.SendMessageBytes(Socket, messageType,
- data, allowToCompressMessage, aes);
- }
- }
- #endregion
-
- #region SendText
- /// <summary>
- /// Send text message with the predefined TextMessage type (type=2).
- /// </summary>
- /// <param name="textMessage">Text message</param>
- public void SendTextMessage(string textMessage)
- {
- // Only send if we have a connectionThread or Socket!
- if (connectionThread != null ||
- Socket != null)
- {
- SendBytes((byte)BasicMessageTypes.TextMessage,
- StringHelper.StringToBytes(textMessage));
- }
- }
- #endregion
-
- #region ToString
- /// <summary>
- /// To string method to display some debug information about this client.
- /// </summary>
- /// <returns>String with detailed client and socket information</returns>
- public override string ToString()
- {
- return "Client " +
- (String.IsNullOrEmpty(ServerAddress) == false
- ? "Server=" + ServerAddress +
- ":" + ServerPorts.Write() + ", "
- : "") +
- "Username=" + Username + ", Socket=" +
- SocketHelper.WriteClientInfo(Socket);
- }
- #endregion
- }
- }