PageRenderTime 147ms CodeModel.GetById 61ms app.highlight 15ms RepoModel.GetById 65ms app.codeStats 0ms

/Utilities/Networking/BaseServer.cs

#
C# | 580 lines | 256 code | 46 blank | 278 comment | 25 complexity | 26bfe0dd2c85b1157810c86fd68aefff MD5 | raw file
  1// Helper to figure out networking issues, will write into console only!
  2//#define LOG_STUFF
  3
  4using System;
  5using System.Collections.Generic;
  6using System.IO;
  7using System.Net;
  8using System.Net.Sockets;
  9using System.Threading;
 10using Delta.Utilities.Cryptography;
 11using Delta.Utilities.Helpers;
 12using Delta.Utilities.Xml;
 13
 14namespace Delta.Utilities.Networking
 15{
 16	/// <summary>
 17	/// Tcp Server base class. Provides useful functionality for server programs
 18	/// like the Log server or the Content and Build Server. Basically listens
 19	/// for clients with a TcpListener and manages a list of Sockets for
 20	/// connected clients. Build on top of the SocketHelper and StreamHelper
 21	/// functionality of the Delta.Utilities.Helpers assembly. Also provides
 22	/// compression functionality automatically for all messages with a payload
 23	/// of over 1024 bytes (like files). Additionally all network traffic can
 24	/// be encrypted with AES cryptography and the initial seed value can even
 25	/// be decrypted with a secret RSA key using a public RSA key on the clients.
 26	/// <para />
 27	/// Please keep in mind that a server usually has many clients that can
 28	/// connect and send data. All client methods like OnClientLogin and
 29	/// OnMessageReceived can be called from different data receiving threads
 30	/// and should be made thread safe (put a lock around critical code blocks).
 31	/// </summary>
 32	public abstract class BaseServer : IDisposable
 33	{
 34		#region Internal
 35
 36		#region privateKey (Internal)
 37		/// <summary>
 38		/// Private key for encryption, see Cryptography class for details, by
 39		/// default this is null and unused. Please note that for encryption the
 40		/// first message send from the client to the server must be the Connect
 41		/// message (message type 0) and it contains the random seed (optionally
 42		/// even encrypted with a public RSA key from the server). See the
 43		/// <see cref="HandleConnectDecryption"/> method for details.
 44		/// <para />
 45		/// Note: The AES decryption instance can be found in each client.
 46		/// Most importantly the Client must also have the same privateKey, which
 47		/// should be kept secret (but even if it is known by an attacker, if you
 48		/// encrypted the random seed with RSA, the transmission is still secure).
 49		/// </summary>
 50		internal byte[] privateKey;
 51		#endregion
 52
 53		#region rsa (Internal)
 54		/// <summary>
 55		/// You can even use an secret RSA key to decrypt incoming client requests
 56		/// (that must have been signed with the matching public RSA key). If this
 57		/// variable is null, no RSA cryptography is used.
 58		/// </summary>
 59		internal RSA rsa;
 60		#endregion
 61
 62		#endregion
 63
 64		#region Protected
 65
 66		#region numOfMessageTypes (Protected)
 67		/// <summary>
 68		/// Number of message types this server supports. Derived classes set this
 69		/// in the constructor and handle each message type in OnMessageReceived.
 70		/// </summary>
 71		protected readonly byte numOfMessageTypes;
 72		#endregion
 73
 74		#region clients (Protected)
 75		/// <summary>
 76		/// List of client sockets current connected, will be enumerated in the
 77		/// HandleClients method, which handles all the connection, disconnection
 78		/// and receiving data logic.
 79		/// </summary>
 80		protected List<BaseClient> clients = new List<BaseClient>();
 81		#endregion
 82
 83		#region shutdownServer (Protected)
 84		/// <summary>
 85		/// Set this variable to true to shutdown all threads and pending
 86		/// operations on this server. Done in the Close method.
 87		/// </summary>
 88		protected bool shutdownServer;
 89		#endregion
 90
 91		#endregion
 92
 93		#region Private
 94
 95		#region listenerThreads (Private)
 96		/// <summary>
 97		/// Helper threads for running the TcpServer in a different thread (because
 98		/// it is blocking in ListenForClients). This starts the data receiving
 99		/// as well (which works completely asynchrony so we don't need an extra
100		/// thread for that). Multiple threads are used for each listener port.
101		/// </summary>
102		private readonly Dictionary<TcpListener, Thread> listenerThreads =
103			new Dictionary<TcpListener, Thread>();
104		#endregion
105
106		#region clientDisconnectThread (Private)
107		/// <summary>
108		/// Because tcpServerThread just listens for clients and adds new ones
109		/// into the clients list, we need this extra thread to remove disconnected
110		/// clients again when the underlying socket is not longer connected.
111		/// </summary>
112		private Thread clientDisconnectThread;
113		#endregion
114
115		#endregion
116
117		#region Constructors
118		/// <summary>
119		/// Create TCP server, which will immediately start to listen on the
120		/// specified TCP port. Listening actually happens in a separate thread
121		/// and handling all the clients and receiving their messages happens in
122		/// another thread (because the listening thread blocks). This constructor
123		/// will return right away and you can continue with your code. Use the
124		/// delegates to be notified when clients connect or disconnect or new
125		/// data arrives. Note: All messages are automatically compressed if they
126		/// have above 1024 bytes of payload (see Delta.Utilities.Compression.Zip).
127		/// <para />
128		/// Encryption is also optionally possible and can be enabled by using a
129		/// private key (null by default, which means unused) and you can even
130		/// use an secret RSA key to decrypt incoming client requests (that must
131		/// have been signed with the matching public RSA key).
132		/// </summary>
133		/// <param name="setIPAddressesToListenOn">Optional list of IP addresses
134		/// to listen on (one for each port). Can be null for IPAddress.Any</param>
135		/// <param name="setListenerPorts">Lists of ports to listen on for
136		/// incoming clients.</param>
137		/// <param name="setNumberOfMessageTypes">
138		/// Set number of message types to be used in all network communications
139		/// with clients (any incoming data with a message type value above this
140		/// is not allowed and will be rejected).
141		/// </param>
142		/// <param name="setPrivateKey">
143		/// Private key for encryption, see Cryptography class for details, by
144		/// default this is null and unused. Please note that for encryption the
145		/// first message send from the client to the server must be the Connect
146		/// message (message type 0) and it contains the random seed (optionally
147		/// even encrypted with a public RSA key from the server).
148		/// Most importantly the ClientData must also have the same privateKey,
149		/// which should be kept secret (but even if it is known by an attacked
150		/// if you encrypted the random seed with RSA, the transmission is still
151		/// secure).
152		/// </param>
153		/// <param name="secretRsaKey">Secret RSA key on the server (2048 bits)
154		/// that is used to decrypt incoming Connect messages from clients, which
155		/// encrypt their seed value in the Connect message with the matching
156		/// public RSA key that only the server can decrypt. Note: To generate
157		/// a secret and public RSA key pair use the CryptographyTests.
158		/// </param>
159		/// <exception cref="ArgumentNullException">
160		/// Thrown if no ports are specified. The BaseServer needs at least one
161		/// port to listen to!
162		/// </exception>
163		/// <exception cref="Exception">
164		/// Thrown if we failed to initialize the TcpListener on given ports.
165		/// </exception>
166		public BaseServer(string[] setIPAddressesToListenOn,
167			int[] setListenerPorts, byte setNumberOfMessageTypes,
168			byte[] setPrivateKey = null, XmlNode secretRsaKey = null)
169		{
170			if (setListenerPorts == null ||
171			    setListenerPorts.Length == 0)
172			{
173				throw new ArgumentNullException(
174					"BaseServer needs at least one port to listen to!");
175			}
176
177			numOfMessageTypes = setNumberOfMessageTypes;
178			privateKey = setPrivateKey;
179			if (privateKey != null &&
180			    secretRsaKey != null)
181			{
182				rsa = new RSA(secretRsaKey);
183			}
184
185			try
186			{
187				int portNum = 0;
188				foreach (int port in setListenerPorts)
189				{
190					// Listener for clients to connect to. Each new client is added to the
191					// clients list. We can have as many listeners as ports we listen to.
192					IPAddress address = IPAddress.Any;
193					if (setIPAddressesToListenOn != null &&
194					    portNum < setIPAddressesToListenOn.Length)
195					{
196						IPAddress.TryParse(setIPAddressesToListenOn[portNum], out address);
197						if (address == null)
198						{
199							address = IPAddress.Any;
200						}
201					}
202					TcpListener listener = new TcpListener(address, port);
203
204					// Create server thread and name it the same way as the used class
205					listenerThreads.Add(listener,
206						ThreadHelper.Start(ListenForClients, listener));
207					portNum++;
208				}
209
210				// And also handle all client disconnects in an extra thread.
211				clientDisconnectThread = ThreadHelper.Start(HandleClientDisconnects);
212			}
213			catch (Exception ex)
214			{
215				throw new Exception(
216					"Failed to initialize the TcpListener on port '" +
217					setListenerPorts.Write() + "', server was not started.",
218					ex);
219			}
220		}
221
222		/// <summary>
223		/// Create TCP server, which will immediately start to listen on the
224		/// specified TCP port. Listening actually happens in a separate thread
225		/// and handling all the clients and receiving their messages happens in
226		/// another thread (because the listening thread blocks). This constructor
227		/// will return right away and you can continue with your code. Use the
228		/// delegates to be notified when clients connect or disconnect or new
229		/// data arrives. Note: All messages are automatically compressed if they
230		/// have above 1024 bytes of payload (see Delta.Utilities.Compression.Zip).
231		/// <para />
232		/// Encryption is also optionally possible and can be enabled by using a
233		/// private key (null by default, which means unused) and you can even
234		/// use an secret RSA key to decrypt incoming client requests (that must
235		/// have been signed with the matching public RSA key).
236		/// </summary>
237		/// <param name="setListenerPort">
238		/// Port to listen on for incoming clients.
239		/// </param>
240		/// <param name="setNumberOfMessageTypes">
241		/// Set number of message types to be used in all network communications
242		/// with clients (any incoming data with a message type value above this
243		/// is not allowed and will be rejected).
244		/// </param>
245		/// <param name="setPrivateKey">
246		/// Private key for encryption, see Cryptography class for details, by
247		/// default this is null and unused. Please note that for encryption the
248		/// first message send from the client to the server must be the Connect
249		/// message (message type 0) and it contains the random seed (optionally
250		/// even encrypted with a public RSA key from the server).
251		/// Most importantly the ClientData must also have the same privateKey,
252		/// which should be kept secret (but even if it is known by an attacked
253		/// if you encrypted the random seed with RSA, the transmission is still
254		/// secure).
255		/// </param>
256		/// <param name="secretRsaKey">Secret RSA key on the server (2048 bits)
257		/// that is used to decrypt incoming Connect messages from clients, which
258		/// encrypt their seed value in the Connect message with the matching
259		/// public RSA key that only the server can decrypt. Note: To generate
260		/// a secret and public RSA key pair use the CryptographyTests.
261		/// </param>
262		public BaseServer(int setListenerPort, byte setNumberOfMessageTypes,
263			byte[] setPrivateKey = null, XmlNode secretRsaKey = null)
264			: this(null, new[]
265			{
266				setListenerPort
267			}, setNumberOfMessageTypes,
268				setPrivateKey, secretRsaKey)
269		{
270		}
271		#endregion
272
273		#region Destructor
274		/// <summary>
275		/// ~Base server
276		/// </summary>
277		~BaseServer()
278		{
279			Dispose();
280		}
281		#endregion
282
283		#region IDisposable Members
284		/// <summary>
285		/// Stop tcp server, should be called at the end of the application.
286		/// </summary>
287		public virtual void Dispose()
288		{
289			// Abort any loops that wait for clients to connect, see ListenForClients
290			shutdownServer = true;
291
292			foreach (TcpListener listener in listenerThreads.Keys)
293			{
294				listener.Stop();
295			}
296			// Give it some time to disconnect and stop all listeners
297			Thread.Sleep(10);
298
299			// Disconnect all clients
300			for (int i = 0; i < clients.Count; i++)
301			{
302				if (clients[i] != null)
303				{
304					clients[i].Dispose();
305					clients[i] = null;
306				}
307			}
308
309			// Clear the clients list
310			clients.Clear();
311
312			// And finally kill all listener threads we created that might still be
313			// open (this might throw ThreadAbortExceptions, but usually those
314			// threads have ended already and we should be fine).
315			foreach (KeyValuePair<TcpListener, Thread> listener in listenerThreads)
316			{
317				listener.Value.Abort();
318			}
319			listenerThreads.Clear();
320
321			shutdownServer = true;
322		}
323		#endregion
324
325		#region SendTextToAllClients (Public)
326		/// <summary>
327		/// Send text message with the predefined TextMessage type.
328		/// </summary>
329		/// <param name="textMessage">Text Message</param>
330		public void SendTextToAllClients(string textMessage)
331		{
332			foreach (BaseClient client in clients)
333			{
334				client.SendTextMessage(textMessage);
335			}
336		}
337		#endregion
338
339		#region Methods (Private)
340
341		#region CreateClient
342		/// <summary>
343		/// Create client data helper method to allow creating derived client data
344		/// classes in derived BaseServer classes.
345		/// </summary>
346		/// <param name="clientSocket">Client Socket</param>
347		/// <returns>
348		/// Client instance for keeping the socket and all client data around as
349		/// long as the server needs this.
350		/// </returns>
351		protected abstract BaseClient CreateClient(Socket clientSocket);
352		#endregion
353
354		#region ListenForClients
355		/// <summary>
356		/// Listen for clients. Note: This method will block in an extra thread
357		/// while waiting for client connections and run forever in an endless
358		/// listening mode. To abort kill the whole thread, which will be catched
359		/// here and the listener will be killed in the finally block.
360		/// </summary>
361		private void ListenForClients(object param)
362		{
363			TcpListener listener = param as TcpListener;
364			try
365			{
366				listener.Start();
367			}
368			catch (Exception ex)
369			{
370				Log.Warning(
371					"Failed to listen on '" + listener.LocalEndpoint + "': " +
372					ex.Message);
373				// End this thread
374				return;
375			}
376
377			try
378			{
379				// Enter the listening loop
380				do
381				{
382					try
383					{
384						// Block until we have a client connecting to us.
385						Socket client = listener.AcceptSocket();
386
387						// Create a new ClientInfo object and add it to our list.
388						BaseClient clientData = CreateClient(client);
389						// Only continue if client is wanted
390						if (clientData == null)
391						{
392							// Abort
393							continue;
394						}
395						clients.Add(clientData);
396
397						// And start listening for incoming data! We have to wait for the
398						// Connect message to process this client any further. Right now
399						// all we have is the socket connection, this is not a real user yet.
400						clientData.StartReceivingMessages();
401					}
402					catch (SocketException)
403					{
404						// Ignore SocketException, which only reports WSACancelBlockingCall
405					}
406					catch
407					{
408					}
409				} while (shutdownServer == false);
410			}
411			finally
412			{
413				// Kill listener to stop accepting new clients
414				listener.Stop();
415				/*wtf, this should not be required!
416				foreach (BaseClient client in clients)
417				{
418					client.Dispose();
419				}
420				 */
421			}
422		}
423		#endregion
424
425		#region HandleClientDisconnects
426		/// <summary>
427		/// Handle client disconnects, loops through all clients and checks if
428		/// they are still connected. Currently just handles disconnects, but
429		/// reconnects could be made possible and Peer-To-Peer logic would be
430		/// more complex, but for now this does not much.
431		/// </summary>
432		private void HandleClientDisconnects()
433		{
434			do
435			{
436				// Cache current number of clients because it might change in the
437				// ListenForClients method. The following check does only handle
438				// disconnects, so the list will change again.
439				int numberOfClients = clients.Count;
440				for (int clientNum = 0; clientNum < numberOfClients; clientNum++)
441				{
442					BaseClient client = clients[clientNum];
443
444					try
445					{
446						// Are we not longer connected? Then remove from the list!
447						if (client.Socket == null ||
448						    client.Socket.Connected == false)
449						{
450							// Notify event handler
451							OnClientDisconnected(client);
452
453							// Shutdown and end connection
454							client.Dispose();
455
456							// And remove from the list
457							clients.Remove(client);
458							numberOfClients--;
459							clientNum--;
460							continue;
461						}
462					}
463					catch
464					{
465					}
466				}
467
468				// Wait a bit on the server (we usually have not much to do)
469				if (shutdownServer == false)
470				{
471					Thread.Sleep(40);
472				}
473			} while (shutdownServer == false);
474		}
475		#endregion
476
477		#region HandleClientLogin
478		/// <summary>
479		/// Handle client login
480		/// </summary>
481		/// <param name="client">Client</param>
482		/// <param name="data">Data</param>
483		internal bool HandleClientLogin(BaseClient client, BinaryReader data)
484		{
485			return OnClientLogin(client, data);
486		}
487		#endregion
488
489		#region OnClientLogin
490		/// <summary>
491		/// Event that is fired every time a new client connects and has send the
492		/// Login message (after the Connect message), see
493		/// <see cref="Client.OnRawMessageReceived"/> for details.
494		/// <para />
495		/// This gives us a chance to handle the Login request here and reject
496		/// clients if anything is wrong (wrong password, ip is banned, etc.).
497		/// Return false if you want to reject the client, you can also send out
498		/// a Login message to the client first with the reason for rejecting him.
499		/// If this method returns true you also must send out a Login message to
500		/// notify the client that he is now connected and logged in.
501		/// </summary>
502		/// <param name="client">Client that send the Login message</param>
503		/// <param name="data">Data the client send for this Login message
504		/// (already decrypted and uncompressed). This is by default just
505		/// the username, but you can send whatever login information you need.
506		/// </param>
507		/// <returns>True if the client is successfully logged in or false if we
508		/// had to reject this client (see Login message for reject reasons).
509		/// </returns>
510		protected virtual bool OnClientLogin(BaseClient client,
511			BinaryReader data)
512		{
513			// Set the username from the Connect message!
514			client.Username = data.ReadString();
515
516			// Note: In derived classes you can also send more login data like
517			// the login status, extra user information, etc. Here just an empty
518			// message is send to notify the client that he is now logged in.
519			client.Send((byte)BasicMessageTypes.Login);
520			return true;
521		}
522		#endregion
523
524		#region OnClientDisconnected
525		/// <summary>
526		/// Event that is fired every time a client disconnects to inform the
527		/// server about the not longer connected client. This can happen on
528		/// purpose by either side or because the connection was disconnected.
529		/// <para />
530		/// Note: Does just call client.Dispose, if you need more server side logic
531		/// implement it in derived classes.
532		/// </summary>
533		/// <param name="client">Client that has been connected. Please note that
534		/// this could even happen before OnClientConnected is even called (but
535		/// this is very unlikely, then the Connect message must be messed up).
536		/// </param>
537		protected virtual void OnClientDisconnected(BaseClient client)
538		{
539			client.Dispose();
540		}
541		#endregion
542
543		#region HandleMessageReceived
544		/// <summary>
545		/// Handle message received
546		/// </summary>
547		/// <param name="client">Client</param>
548		/// <param name="messageType">Message type</param>
549		/// <param name="data">Data</param>
550		internal void HandleMessageReceived(BaseClient client,
551			byte messageType, BinaryReader data)
552		{
553			OnMessageReceived(client, messageType, data);
554		}
555		#endregion
556
557		#region OnMessageReceived
558		/// <summary>
559		/// Event that is fired every time a full message is received, called by
560		/// <see cref="BaseClient.OnRawMessageReceived"/>. Note: This is not the
561		/// same as receiving raw network data as a new data package might not
562		/// contain a full message yet, or even contain multiple message for that
563		/// matter. Additionally decompression and decryption is already handled.
564		/// <para />
565		/// Each message consists of a message type (byte) and the data (bytes).
566		/// Note: OnPreCheckMessage is not supported by default in
567		/// BaseServer, but you can easily add it if needed (for checking big
568		/// messages) or just use the Client.OnPreCheckMessage method.
569		/// </summary>
570		/// <param name="client">Client we are receiving data for</param>
571		/// <param name="messageType">Message type we received</param>
572		/// <param name="data">Data we have received for this message type
573		/// (already uncompressed and decrypted)</param>
574		protected internal abstract void OnMessageReceived(BaseClient client,
575			byte messageType, BinaryReader data);
576		#endregion
577
578		#endregion
579	}
580}