/Utilities/Networking/BaseServer.cs
C# | 580 lines | 256 code | 46 blank | 278 comment | 25 complexity | 26bfe0dd2c85b1157810c86fd68aefff 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;
- using System.Net.Sockets;
- using System.Threading;
- using Delta.Utilities.Cryptography;
- using Delta.Utilities.Helpers;
- using Delta.Utilities.Xml;
-
- namespace Delta.Utilities.Networking
- {
- /// <summary>
- /// Tcp Server base class. Provides useful functionality for server programs
- /// like the Log server or the Content and Build Server. Basically listens
- /// for clients with a TcpListener and manages a list of Sockets for
- /// connected clients. Build on top of the SocketHelper and StreamHelper
- /// functionality of the Delta.Utilities.Helpers assembly. Also provides
- /// compression functionality automatically for all messages with a payload
- /// of over 1024 bytes (like files). Additionally all network traffic can
- /// be encrypted with AES cryptography and the initial seed value can even
- /// be decrypted with a secret RSA key using a public RSA key on the clients.
- /// <para />
- /// Please keep in mind that a server usually has many clients that can
- /// connect and send data. All client methods like OnClientLogin and
- /// OnMessageReceived can be called from different data receiving threads
- /// and should be made thread safe (put a lock around critical code blocks).
- /// </summary>
- public abstract class BaseServer : IDisposable
- {
- #region Internal
-
- #region privateKey (Internal)
- /// <summary>
- /// 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). See the
- /// <see cref="HandleConnectDecryption"/> method for details.
- /// <para />
- /// Note: The AES decryption instance can be found in each client.
- /// Most importantly the Client must also have the same privateKey, which
- /// should be kept secret (but even if it is known by an attacker, if you
- /// encrypted the random seed with RSA, the transmission is still secure).
- /// </summary>
- internal byte[] privateKey;
- #endregion
-
- #region rsa (Internal)
- /// <summary>
- /// You can even use an secret RSA key to decrypt incoming client requests
- /// (that must have been signed with the matching public RSA key). If this
- /// variable is null, no RSA cryptography is used.
- /// </summary>
- internal RSA rsa;
- #endregion
-
- #endregion
-
- #region Protected
-
- #region numOfMessageTypes (Protected)
- /// <summary>
- /// Number of message types this server supports. Derived classes set this
- /// in the constructor and handle each message type in OnMessageReceived.
- /// </summary>
- protected readonly byte numOfMessageTypes;
- #endregion
-
- #region clients (Protected)
- /// <summary>
- /// List of client sockets current connected, will be enumerated in the
- /// HandleClients method, which handles all the connection, disconnection
- /// and receiving data logic.
- /// </summary>
- protected List<BaseClient> clients = new List<BaseClient>();
- #endregion
-
- #region shutdownServer (Protected)
- /// <summary>
- /// Set this variable to true to shutdown all threads and pending
- /// operations on this server. Done in the Close method.
- /// </summary>
- protected bool shutdownServer;
- #endregion
-
- #endregion
-
- #region Private
-
- #region listenerThreads (Private)
- /// <summary>
- /// Helper threads for running the TcpServer in a different thread (because
- /// it is blocking in ListenForClients). This starts the data receiving
- /// as well (which works completely asynchrony so we don't need an extra
- /// thread for that). Multiple threads are used for each listener port.
- /// </summary>
- private readonly Dictionary<TcpListener, Thread> listenerThreads =
- new Dictionary<TcpListener, Thread>();
- #endregion
-
- #region clientDisconnectThread (Private)
- /// <summary>
- /// Because tcpServerThread just listens for clients and adds new ones
- /// into the clients list, we need this extra thread to remove disconnected
- /// clients again when the underlying socket is not longer connected.
- /// </summary>
- private Thread clientDisconnectThread;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create TCP server, which will immediately start to listen on the
- /// specified TCP port. Listening actually happens in a separate thread
- /// and handling all the clients and receiving their messages happens in
- /// another thread (because the listening thread blocks). This constructor
- /// will return right away and you can continue with your code. Use the
- /// delegates to be notified when clients connect or disconnect or new
- /// data arrives. Note: All messages are automatically compressed if they
- /// have above 1024 bytes of payload (see Delta.Utilities.Compression.Zip).
- /// <para />
- /// Encryption is also optionally possible and can be enabled by using a
- /// private key (null by default, which means unused) and you can even
- /// use an secret RSA key to decrypt incoming client requests (that must
- /// have been signed with the matching public RSA key).
- /// </summary>
- /// <param name="setIPAddressesToListenOn">Optional list of IP addresses
- /// to listen on (one for each port). Can be null for IPAddress.Any</param>
- /// <param name="setListenerPorts">Lists of ports to listen on for
- /// incoming clients.</param>
- /// <param name="setNumberOfMessageTypes">
- /// Set number of message types to be used in all network communications
- /// with clients (any incoming data with a message type value above this
- /// is not allowed and will be rejected).
- /// </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="secretRsaKey">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 with the matching
- /// public RSA key that only the server can decrypt. Note: To generate
- /// a secret and public RSA key pair use the CryptographyTests.
- /// </param>
- /// <exception cref="ArgumentNullException">
- /// Thrown if no ports are specified. The BaseServer needs at least one
- /// port to listen to!
- /// </exception>
- /// <exception cref="Exception">
- /// Thrown if we failed to initialize the TcpListener on given ports.
- /// </exception>
- public BaseServer(string[] setIPAddressesToListenOn,
- int[] setListenerPorts, byte setNumberOfMessageTypes,
- byte[] setPrivateKey = null, XmlNode secretRsaKey = null)
- {
- if (setListenerPorts == null ||
- setListenerPorts.Length == 0)
- {
- throw new ArgumentNullException(
- "BaseServer needs at least one port to listen to!");
- }
-
- numOfMessageTypes = setNumberOfMessageTypes;
- privateKey = setPrivateKey;
- if (privateKey != null &&
- secretRsaKey != null)
- {
- rsa = new RSA(secretRsaKey);
- }
-
- try
- {
- int portNum = 0;
- foreach (int port in setListenerPorts)
- {
- // Listener for clients to connect to. Each new client is added to the
- // clients list. We can have as many listeners as ports we listen to.
- IPAddress address = IPAddress.Any;
- if (setIPAddressesToListenOn != null &&
- portNum < setIPAddressesToListenOn.Length)
- {
- IPAddress.TryParse(setIPAddressesToListenOn[portNum], out address);
- if (address == null)
- {
- address = IPAddress.Any;
- }
- }
- TcpListener listener = new TcpListener(address, port);
-
- // Create server thread and name it the same way as the used class
- listenerThreads.Add(listener,
- ThreadHelper.Start(ListenForClients, listener));
- portNum++;
- }
-
- // And also handle all client disconnects in an extra thread.
- clientDisconnectThread = ThreadHelper.Start(HandleClientDisconnects);
- }
- catch (Exception ex)
- {
- throw new Exception(
- "Failed to initialize the TcpListener on port '" +
- setListenerPorts.Write() + "', server was not started.",
- ex);
- }
- }
-
- /// <summary>
- /// Create TCP server, which will immediately start to listen on the
- /// specified TCP port. Listening actually happens in a separate thread
- /// and handling all the clients and receiving their messages happens in
- /// another thread (because the listening thread blocks). This constructor
- /// will return right away and you can continue with your code. Use the
- /// delegates to be notified when clients connect or disconnect or new
- /// data arrives. Note: All messages are automatically compressed if they
- /// have above 1024 bytes of payload (see Delta.Utilities.Compression.Zip).
- /// <para />
- /// Encryption is also optionally possible and can be enabled by using a
- /// private key (null by default, which means unused) and you can even
- /// use an secret RSA key to decrypt incoming client requests (that must
- /// have been signed with the matching public RSA key).
- /// </summary>
- /// <param name="setListenerPort">
- /// Port to listen on for incoming clients.
- /// </param>
- /// <param name="setNumberOfMessageTypes">
- /// Set number of message types to be used in all network communications
- /// with clients (any incoming data with a message type value above this
- /// is not allowed and will be rejected).
- /// </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="secretRsaKey">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 with the matching
- /// public RSA key that only the server can decrypt. Note: To generate
- /// a secret and public RSA key pair use the CryptographyTests.
- /// </param>
- public BaseServer(int setListenerPort, byte setNumberOfMessageTypes,
- byte[] setPrivateKey = null, XmlNode secretRsaKey = null)
- : this(null, new[]
- {
- setListenerPort
- }, setNumberOfMessageTypes,
- setPrivateKey, secretRsaKey)
- {
- }
- #endregion
-
- #region Destructor
- /// <summary>
- /// ~Base server
- /// </summary>
- ~BaseServer()
- {
- Dispose();
- }
- #endregion
-
- #region IDisposable Members
- /// <summary>
- /// Stop tcp server, should be called at the end of the application.
- /// </summary>
- public virtual void Dispose()
- {
- // Abort any loops that wait for clients to connect, see ListenForClients
- shutdownServer = true;
-
- foreach (TcpListener listener in listenerThreads.Keys)
- {
- listener.Stop();
- }
- // Give it some time to disconnect and stop all listeners
- Thread.Sleep(10);
-
- // Disconnect all clients
- for (int i = 0; i < clients.Count; i++)
- {
- if (clients[i] != null)
- {
- clients[i].Dispose();
- clients[i] = null;
- }
- }
-
- // Clear the clients list
- clients.Clear();
-
- // And finally kill all listener threads we created that might still be
- // open (this might throw ThreadAbortExceptions, but usually those
- // threads have ended already and we should be fine).
- foreach (KeyValuePair<TcpListener, Thread> listener in listenerThreads)
- {
- listener.Value.Abort();
- }
- listenerThreads.Clear();
-
- shutdownServer = true;
- }
- #endregion
-
- #region SendTextToAllClients (Public)
- /// <summary>
- /// Send text message with the predefined TextMessage type.
- /// </summary>
- /// <param name="textMessage">Text Message</param>
- public void SendTextToAllClients(string textMessage)
- {
- foreach (BaseClient client in clients)
- {
- client.SendTextMessage(textMessage);
- }
- }
- #endregion
-
- #region Methods (Private)
-
- #region CreateClient
- /// <summary>
- /// Create client data helper method to allow creating derived client data
- /// classes in derived BaseServer classes.
- /// </summary>
- /// <param name="clientSocket">Client Socket</param>
- /// <returns>
- /// Client instance for keeping the socket and all client data around as
- /// long as the server needs this.
- /// </returns>
- protected abstract BaseClient CreateClient(Socket clientSocket);
- #endregion
-
- #region ListenForClients
- /// <summary>
- /// Listen for clients. Note: This method will block in an extra thread
- /// while waiting for client connections and run forever in an endless
- /// listening mode. To abort kill the whole thread, which will be catched
- /// here and the listener will be killed in the finally block.
- /// </summary>
- private void ListenForClients(object param)
- {
- TcpListener listener = param as TcpListener;
- try
- {
- listener.Start();
- }
- catch (Exception ex)
- {
- Log.Warning(
- "Failed to listen on '" + listener.LocalEndpoint + "': " +
- ex.Message);
- // End this thread
- return;
- }
-
- try
- {
- // Enter the listening loop
- do
- {
- try
- {
- // Block until we have a client connecting to us.
- Socket client = listener.AcceptSocket();
-
- // Create a new ClientInfo object and add it to our list.
- BaseClient clientData = CreateClient(client);
- // Only continue if client is wanted
- if (clientData == null)
- {
- // Abort
- continue;
- }
- clients.Add(clientData);
-
- // And start listening for incoming data! We have to wait for the
- // Connect message to process this client any further. Right now
- // all we have is the socket connection, this is not a real user yet.
- clientData.StartReceivingMessages();
- }
- catch (SocketException)
- {
- // Ignore SocketException, which only reports WSACancelBlockingCall
- }
- catch
- {
- }
- } while (shutdownServer == false);
- }
- finally
- {
- // Kill listener to stop accepting new clients
- listener.Stop();
- /*wtf, this should not be required!
- foreach (BaseClient client in clients)
- {
- client.Dispose();
- }
- */
- }
- }
- #endregion
-
- #region HandleClientDisconnects
- /// <summary>
- /// Handle client disconnects, loops through all clients and checks if
- /// they are still connected. Currently just handles disconnects, but
- /// reconnects could be made possible and Peer-To-Peer logic would be
- /// more complex, but for now this does not much.
- /// </summary>
- private void HandleClientDisconnects()
- {
- do
- {
- // Cache current number of clients because it might change in the
- // ListenForClients method. The following check does only handle
- // disconnects, so the list will change again.
- int numberOfClients = clients.Count;
- for (int clientNum = 0; clientNum < numberOfClients; clientNum++)
- {
- BaseClient client = clients[clientNum];
-
- try
- {
- // Are we not longer connected? Then remove from the list!
- if (client.Socket == null ||
- client.Socket.Connected == false)
- {
- // Notify event handler
- OnClientDisconnected(client);
-
- // Shutdown and end connection
- client.Dispose();
-
- // And remove from the list
- clients.Remove(client);
- numberOfClients--;
- clientNum--;
- continue;
- }
- }
- catch
- {
- }
- }
-
- // Wait a bit on the server (we usually have not much to do)
- if (shutdownServer == false)
- {
- Thread.Sleep(40);
- }
- } while (shutdownServer == false);
- }
- #endregion
-
- #region HandleClientLogin
- /// <summary>
- /// Handle client login
- /// </summary>
- /// <param name="client">Client</param>
- /// <param name="data">Data</param>
- internal bool HandleClientLogin(BaseClient client, BinaryReader data)
- {
- return OnClientLogin(client, data);
- }
- #endregion
-
- #region OnClientLogin
- /// <summary>
- /// Event that is fired every time a new client connects and has send the
- /// Login message (after the Connect message), see
- /// <see cref="Client.OnRawMessageReceived"/> for details.
- /// <para />
- /// This gives us a chance to handle the Login request here and reject
- /// clients if anything is wrong (wrong password, ip is banned, etc.).
- /// Return false if you want to reject the client, you can also send out
- /// a Login message to the client first with the reason for rejecting him.
- /// If this method returns true you also must send out a Login message to
- /// notify the client that he is now connected and logged in.
- /// </summary>
- /// <param name="client">Client that send the Login message</param>
- /// <param name="data">Data the client send for this Login message
- /// (already decrypted and uncompressed). This is by default just
- /// the username, but you can send whatever login information you need.
- /// </param>
- /// <returns>True if the client is successfully logged in or false if we
- /// had to reject this client (see Login message for reject reasons).
- /// </returns>
- protected virtual bool OnClientLogin(BaseClient client,
- BinaryReader data)
- {
- // Set the username from the Connect message!
- client.Username = data.ReadString();
-
- // Note: In derived classes you can also send more login data like
- // the login status, extra user information, etc. Here just an empty
- // message is send to notify the client that he is now logged in.
- client.Send((byte)BasicMessageTypes.Login);
- return true;
- }
- #endregion
-
- #region OnClientDisconnected
- /// <summary>
- /// Event that is fired every time a client disconnects to inform the
- /// server about the not longer connected client. This can happen on
- /// purpose by either side or because the connection was disconnected.
- /// <para />
- /// Note: Does just call client.Dispose, if you need more server side logic
- /// implement it in derived classes.
- /// </summary>
- /// <param name="client">Client that has been connected. Please note that
- /// this could even happen before OnClientConnected is even called (but
- /// this is very unlikely, then the Connect message must be messed up).
- /// </param>
- protected virtual void OnClientDisconnected(BaseClient client)
- {
- client.Dispose();
- }
- #endregion
-
- #region HandleMessageReceived
- /// <summary>
- /// Handle message received
- /// </summary>
- /// <param name="client">Client</param>
- /// <param name="messageType">Message type</param>
- /// <param name="data">Data</param>
- internal void HandleMessageReceived(BaseClient client,
- byte messageType, BinaryReader data)
- {
- OnMessageReceived(client, messageType, data);
- }
- #endregion
-
- #region OnMessageReceived
- /// <summary>
- /// Event that is fired every time a full message is received, called by
- /// <see cref="BaseClient.OnRawMessageReceived"/>. Note: This is not the
- /// same as receiving raw network data as a new data package might not
- /// contain a full message yet, or even contain multiple message for that
- /// matter. Additionally decompression and decryption is already handled.
- /// <para />
- /// Each message consists of a message type (byte) and the data (bytes).
- /// Note: OnPreCheckMessage is not supported by default in
- /// BaseServer, but you can easily add it if needed (for checking big
- /// messages) or just use the Client.OnPreCheckMessage method.
- /// </summary>
- /// <param name="client">Client we are receiving data for</param>
- /// <param name="messageType">Message type we received</param>
- /// <param name="data">Data we have received for this message type
- /// (already uncompressed and decrypted)</param>
- protected internal abstract void OnMessageReceived(BaseClient client,
- byte messageType, BinaryReader data);
- #endregion
-
- #endregion
- }
- }