PageRenderTime 67ms CodeModel.GetById 33ms app.highlight 25ms RepoModel.GetById 1ms app.codeStats 0ms

/Utilities/Networking/SocketHelper.cs

#
C# | 694 lines | 433 code | 53 blank | 208 comment | 67 complexity | f89627692197366f682a4459a9b222fb MD5 | raw file
  1// Warning: When LOG_STUFF is on and LogServer is used, we could get in trouble
  2//#define LOG_STUFF
  3
  4using System;
  5using System.Diagnostics;
  6using System.IO;
  7using System.Net;
  8using System.Net.Sockets;
  9using System.Threading;
 10using Delta.Utilities.Compression;
 11using Delta.Utilities.Cryptography;
 12using Delta.Utilities.Helpers;
 13
 14namespace Delta.Utilities.Networking
 15{
 16	/// <summary>
 17	/// SocketHelper class, stuff used wherever network stuff is needed!
 18	/// </summary>
 19	public static class SocketHelper
 20	{
 21		#region Constants
 22		/// <summary>
 23		/// Receive up to 8 low level packets with 1440 (1500 is MTU -40 bytes for
 24		/// TCP/IP -20 bytes for different needs (e.g. VPN uses some bytes)) data.
 25		/// Bigger messages than this will be put together inside the
 26		/// OnReceivedDataAsyncCallback method with multiple calls to Receive.
 27		/// Most messages will be way smaller (few bytes).
 28		/// </summary>
 29		internal const int ReceiveBufferSize = 1440 * 8; //128;//8192;
 30
 31		/// <summary>
 32		/// Maximum message size for one packet should not go beyond 128 MB!
 33		/// Usually there is something wrong if we get messages this big.
 34		/// </summary>
 35		private const int MaxMessageLength = 1024 * 1024 * 128;
 36
 37		/// <summary>
 38		/// Indicator for compressed messages (done before encryption). This will
 39		/// be saved as an extra byte right before the message type (this message
 40		/// type number is not allowed).
 41		/// </summary>
 42		private const byte CompressionIndicatorByteValue = 255;
 43
 44		/// <summary>
 45		/// Message that will be displayed when a browser tries to connect to this
 46		/// service! The connection will be closed.
 47		/// </summary>
 48		public static string ThisIsNotAWebserverMessage =
 49			"This is not a webserver, unable to continue. Please use the correct " +
 50			"tool to connect to this service!";
 51		#endregion
 52
 53		#region OnReceivedDataAsyncCallback (Static)
 54		/// <summary>
 55		/// On received data async callback. Public to allow overridden 
 56		/// ClientData.StartReceivingMessagesAndSendConnectMessage to use this.
 57		/// </summary>
 58		/// <param name="ar">AsyncState, which holds the BaseClient</param>
 59		public static void OnReceivedDataAsyncCallback(IAsyncResult ar)
 60		{
 61			BaseClient data = null;
 62			try
 63			{
 64				data = (BaseClient)ar.AsyncState;
 65				if (data == null ||
 66				    data.Socket == null ||
 67				    data.Socket.Connected == false)
 68				{
 69					// We got no socket to receive anything, abort!
 70					return;
 71				}
 72
 73				// Make sure we handle one received data block at a time!
 74				lock (data)
 75				{
 76					int numOfReceivedBytes = data.Socket.EndReceive(ar);
 77					if (numOfReceivedBytes == 0)
 78					{
 79						// Nothing received, close connection. This is normal for
 80						// disconnecting sockets in TCP/IP
 81						if (data.OnDisconnected != null)
 82						{
 83							data.OnDisconnected();
 84						}
 85						data.Dispose();
 86						return;
 87					}
 88
 89					// Otherwise everything is fine, handle the received data and
 90					// fire the messageReceived event for each received message.
 91					try
 92					{
 93						ReceiveMessageData(data, numOfReceivedBytes);
 94					}
 95					catch (SocketException)
 96					{
 97						// Note: OnLogin will be called to notify the caller
 98						data.Dispose();
 99						if (data.OnDisconnected != null)
100						{
101							data.OnDisconnected();
102						}
103					}
104					catch (Exception ex)
105					{
106						Log.Warning("ReceiveMessageData failed: " + ex);
107					}
108
109					// And finally setup the receive callback again if we are still
110					// connected (there might have been a forced disconnect)!
111					if (data.Socket != null)
112					{
113						SetupReceiveCallback(data.Socket, data.clientBufferData,
114							OnReceivedDataAsyncCallback, data);
115					}
116				}
117			}
118			catch (SocketException)
119			{
120				// Disconnect client, this happened: SocketException (0x80004005):
121				// An existing connection was forcibly closed by the remote host
122				if (data != null)
123				{
124					// Note: OnLogin will be called to notify the caller if this happened
125					// before a connection was established, and we also call
126					// OnDisconnected in case the user wants to know when this happened.
127					try
128					{
129						data.Dispose();
130						if (data.OnDisconnected != null)
131						{
132							data.OnDisconnected();
133						}
134					}
135					catch (Exception ex)
136					{
137						Log.Warning("OnReceivedData failed to dispose client: " + ex);
138					}
139				}
140			}
141			catch (Exception ex)
142			{
143				Log.Warning("OnReceivedData failed: " + ex);
144			}
145		}
146		#endregion
147
148
149		#region SendMessageBytes (Static)
150		/// <summary>
151		/// Send data to socket, will build packet with length of message, and then
152		/// add the type and message data (if there is any). A message has a
153		/// minimum size of 2 bytes (length and type, e.g. 1,10 indicates there is
154		/// a message with 1 bytes: message type = 10 with no extra data).
155		/// <para />
156		/// This method does handle all the compression and optionally encryption
157		/// logic that can be enabled on the Server and Client side. The receiving
158		/// side is handled in <see cref="BaseClient.OnRawMessageReceived"/>.
159		/// </summary>
160		/// <param name="socket">Socket to send the data to</param>
161		/// <param name="messageType">Type of message (as byte)</param>
162		/// <param name="data">Data to send</param>
163		/// <param name="allowToCompressMessage">
164		/// Allow to automatically compress the message payload data if it is above
165		/// 1024 bytes (otherwise nothing will happen), on by default. An extra
166		/// byte with 0xFF (255) is sent to mark that the incoming message is
167		/// compressed.
168		/// </param>
169		/// <param name="encryptMessage">Encript this message?</param>
170		public static void SendMessageBytes(Socket socket, byte messageType,
171			byte[] data, bool allowToCompressMessage, AES encryptMessage = null)
172		{
173			// Not connected? Then simply don't send!
174			if (socket == null ||
175			    socket.Connected == false)
176			{
177				return;
178			}
179
180			MemoryStream memStream = new MemoryStream();
181			BinaryWriter writer = new BinaryWriter(memStream);
182			// How big is this message? Including the messageType, so each
183			// message is at least this number plus 1 byte of data length.
184			int messageLength =
185				1 +
186				(data != null
187				 	? data.Length
188				 	: 0);
189			int originalDataLength = 0;
190			// Only compress messages above 1024 bytes of payload data!
191			if (data != null &&
192			    // Do not compress or encrypt the connect message, we need it to
193			    // setup the encryption system and we cannot decrypt without the data!
194			    messageType != (byte)BasicMessageTypes.Connect)
195			{
196				// First handle compression (if the message is big enough)
197				if (allowToCompressMessage &&
198				    data.Length >= Zip.MinimumByteDataLengthToZip)
199				{
200					// Compress the data, but only keep it if it is smaller
201					byte[] compressedData = Zip.Compress(data);
202					if (compressedData.Length < data.Length)
203					{
204						data = compressedData;
205						// Add an extra byte with 0xFF (255) to mark as compressed!
206						messageLength = 1 + 1 + data.Length;
207					}
208					else
209					{
210						// Disable compression, not worth it, keep existing message length
211						allowToCompressMessage = false;
212					}
213				}
214				else
215				{
216					// Otherwise no compression is used
217					allowToCompressMessage = false;
218				}
219
220				// Next encrypt the message if enabled
221				if (encryptMessage != null)
222				{
223					// Remember the original data length (already might be compressed)
224					originalDataLength = data.Length;
225					// Encrypt the data
226					data = encryptMessage.Encrypt(data);
227					// Now calculate the new amount of bytes we have to save (see below)
228					// The length is the message type (1 byte), if compression is on
229					// (1 byte), the original data length (1-7 bytes) and the encrypted
230					// data (data.Length).
231					messageLength =
232						1 +
233						(allowToCompressMessage
234						 	? 1
235						 	: 0) +
236						StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(
237							originalDataLength) + data.Length;
238				}
239			}
240			else
241			{
242				// Otherwise no compression is used
243				allowToCompressMessage = false;
244			}
245
246			// Write out the total length of the message. We expect most messages
247			// to be small (below 254 bytes), but even for very big messages the
248			// overhead is only 1 byte to store int length values.
249			StreamHelper.WriteNumberMostlySmallerThan254(writer, messageLength);
250
251			// Mark this as compressed message to make sure we decompress it on
252			// the receiver side (this will make the message 1 byte longer, but
253			// we only do this for big messages and compression saves lots of bytes)
254			if (allowToCompressMessage)
255			{
256				writer.Write(CompressionIndicatorByteValue);
257			}
258			// Next store the message type
259			writer.Write(messageType);
260
261			// If the data was encrypted above, save the original data length
262			if (originalDataLength > 0)
263			{
264				StreamHelper.WriteNumberMostlySmallerThan254(writer,
265					originalDataLength);
266			}
267
268			// And finally store the data itself
269			if (data != null)
270			{
271				// Write out the data we got as input here (already optionally
272				// compressed and encrypted, see above).
273				writer.Write(data);
274			}
275
276			try
277			{
278				// Begin sending the data to the remote device.
279				socket.BeginSend(memStream.GetBuffer(), 0, (int)memStream.Length,
280					SocketFlags.None, SendCallback, socket);
281			}
282			catch
283			{
284				//don't throw error or log it, just ignore (happens when closing)!
285			}
286
287			// We don't need the memory stream anymore, it can safely be disposed
288			memStream.Dispose();
289		}
290		#endregion
291
292		#region ConnectTcpSocket (Static)
293		/// <summary>
294		/// Helper method to simply connect a socket to a specific server
295		/// by the given address and port number. Used mainly for unit tests, but
296		/// thanks to the timeout parameter also really useful in applications!
297		/// </summary>
298		/// <param name="address">The server address</param>
299		/// <param name="port">The servers port number</param>
300		/// <returns>The connected socket or null if anything failed (timeout or
301		/// some other failure, which will be logged btw).</returns>
302		public static Socket ConnectTcpSocket(string address, int port)
303		{
304			int timeoutMs = 750; //666//500
305			return ConnectTcpSocket(address, new[]
306			{
307				port
308			}, timeoutMs, true);
309		}
310
311		/// <summary>
312		/// Connect a socket to the specified address and port and abort if
313		/// the specified timeout exceeded.
314		/// </summary>
315		/// <param name="address">
316		/// The IP or DNS name of the host we want to connect to.
317		/// </param>
318		/// <param name="ports">
319		/// The port numbers of the host, usually just a single port.
320		/// </param>
321		/// <param name="timeoutMs">The timeout in milliseconds after which
322		/// we abort the connecting process if nothing happened yet.</param>
323		/// <param name="warnIfConnectFailed">Warn if the connection failed,
324		/// which is on by default, but sometimes we don't care.</param>
325		/// <returns></returns>
326		public static Socket ConnectTcpSocket(string address, int[] ports,
327			int timeoutMs, bool warnIfConnectFailed)
328		{
329			if (ports == null ||
330			    ports.Length == 0)
331			{
332				throw new ArgumentNullException(
333					"Unable to connect to tcp socket at '" + address +
334					"' without given port!");
335			}
336
337			try
338			{
339				LastConnectionError = "";
340
341				// Now create end point for connecting for each specified port until
342				// we could successfully connect.
343				foreach (int port in ports)
344				{
345					DnsEndPoint logServerIp = new DnsEndPoint(address, port);
346					// Create our socket
347					Socket serverSocket = new Socket(AddressFamily.InterNetwork,
348						SocketType.Stream, ProtocolType.Tcp);
349
350					// And finally connect to the server using a timeout (0.5 seconds)
351					IAsyncResult result = serverSocket.BeginConnect(logServerIp,
352						null, null);
353					bool success = result.AsyncWaitHandle.WaitOne(timeoutMs, true);
354					if (success == false ||
355					    serverSocket.Connected == false)
356					{
357						// Failed to connect server in given timeout!
358						serverSocket.Close();
359					}
360						// Else we had success
361					else
362					{
363						serverSocket.EndConnect(result);
364						return serverSocket;
365					}
366				} // foreach
367
368				// All connection attempts failed, return null
369				return null;
370			}
371			catch (ThreadAbortException)
372			{
373				// Re-throw, caller must handle this (might want to abort other stuff)
374				throw;
375			}
376			catch (Exception ex)
377			{
378				LastConnectionError = ex.Message;
379				if (warnIfConnectFailed)
380				{
381					Log.Warning("Failed to connect to tcp server (" + address + ":" +
382					            ports.Write() + "): " + ex);
383				}
384				return null;
385			}
386		}
387		#endregion
388
389
390		#region WriteClientInfo (Static)
391		/// <summary>
392		/// Write client info helper to get more details about a client socket.
393		/// This is basically the RemoteEndPoint (IP and port) if available,
394		/// otherwise "&lt;null&gt;" or an error is returned.
395		/// </summary>
396		public static string WriteClientInfo(Socket client)
397		{
398			if (client == null ||
399			    client.Connected == false)
400			{
401				return "<Null>";
402			}
403
404			try
405			{
406				return client.RemoteEndPoint.ToString();
407			}
408			catch (Exception ex)
409			{
410				return "Unable to return WriteClientInfo: " + ex;
411			}
412		}
413		#endregion
414
415		#region GetIP (Static)
416		/// <summary>
417		/// Basically the same as WriteClient, but will just return the remote IP
418		/// without the used remote port.
419		/// </summary>
420		public static string GetIP(Socket client)
421		{
422			if (client == null ||
423			    client.Connected == false)
424			{
425				return "<Null>";
426			}
427
428			try
429			{
430				return (client.RemoteEndPoint as IPEndPoint).Address.ToString();
431			}
432			catch (Exception ex)
433			{
434				return "GetIP failed: " + ex.Message;
435			}
436		}
437		#endregion
438
439		#region LastConnectionError (Static)
440		/// <summary>
441		/// Last connection error we got in ConnectTcpSocket (if we got any).
442		/// </summary>
443		public static string LastConnectionError = "";
444		#endregion
445
446		#region Methods (Private)
447
448		#region SetupReceiveCallback
449		/// <summary>
450		/// Setup the receive callback stuff. Used to initially setup the
451		/// receive callback and then every time in GetReceivedData!
452		/// </summary>
453		/// <param name="clientSocket">Client socket for receiving data</param>
454		/// <param name="buffer">Client buffer for the data to receive</param>
455		/// <param name="receiveCallback">
456		/// Receive callback, which is executed once we receive some data on this
457		/// socket.
458		/// </param>
459		/// <param name="obj">
460		/// Object for custom user data (usually BaseClient).
461		/// </param>
462		/// <returns>True if we received something, false otherwise</returns>
463		internal static bool SetupReceiveCallback(Socket clientSocket,
464			byte[] buffer, AsyncCallback receiveCallback, object obj)
465		{
466			if (clientSocket == null ||
467			    clientSocket.Connected == false)
468			{
469				return false;
470			}
471			clientSocket.BeginReceive(buffer, 0, buffer.Length,
472				SocketFlags.None, receiveCallback, obj);
473			return true;
474		}
475		#endregion
476
477
478		#region ReceiveMessageData
479		/// <summary>
480		/// Handle all the receive data issues, extract the message length and
481		/// determines if we need more data and will wait for next call
482		/// in this case. Will also read type of message and check if its
483		/// valid. You can use preCheckMessage to perform some operations while
484		/// message is still not complete. Finally use handleMessage to
485		/// perform the message action, etc.
486		/// </summary>
487		/// <param name="data">Client for data to be received</param>
488		/// <param name="numOfReceivedBytes">num of received bytes</param>
489		private static void ReceiveMessageData(BaseClient data,
490			int numOfReceivedBytes)
491		{
492			// Build memory stream from rememberLastData and the current received
493			// data in clientBufferData, but only use numOfReceivedBytes.
494			MemoryStream memStream = new MemoryStream();
495			// Was there anything remember from last time?
496			if (data.rememberLastData != null)
497			{
498				memStream.Write(data.rememberLastData, 0,
499					data.rememberLastData.Length);
500				data.rememberLastData = null;
501			}
502			// And write how much data we received just now
503			memStream.Write(data.clientBufferData, 0, numOfReceivedBytes);
504
505			// Finally reset the stream position and read the data back
506			memStream.Seek(0, SeekOrigin.Begin);
507			BinaryReader reader = new BinaryReader(memStream);
508			// Loop until we have all data processed (might be several messages)
509			do
510			{
511				// Remember memStreamPos if we want to remember data for later use!
512				int remRestDataStartPos = (int)memStream.Position;
513				int remainingDataLength = (int)(memStream.Length - memStream.Position);
514
515				// Can't continue without at least the length of message
516				int messageLength;
517				if (StreamHelper.TryToGetNumberMostlySmallerThan254(reader,
518					out messageLength) == false ||
519				    // We need one more byte to read the message type!
520				    memStream.Length - memStream.Position < 1)
521				{
522					// Remember data, try again when more data has arrived
523					data.rememberLastData = new byte[remainingDataLength];
524					memStream.Seek(remRestDataStartPos, SeekOrigin.Begin);
525					int bytesRead = memStream.Read(data.rememberLastData, 0,
526						remainingDataLength);
527					// Wait until more data is available!
528					return;
529				}
530
531				// Read the message type and reduce messageLength by one.
532				byte messageType = reader.ReadByte();
533
534				// Is this a Http request? E.g.
535				// GET /index.html HTTP/1.1 ...
536				if (messageLength == (byte)'G' &&
537					messageType == (byte)'E')
538				{
539					char nextChar = (char)reader.PeekChar();
540					if (nextChar == 'T')
541					{
542						// Then abort and send a dummy webpage back to the client
543						// and disconnect him, this is not a HTTP server!
544						MemoryStream response = new MemoryStream();
545						BinaryWriter responseWriter = new BinaryWriter(response);
546						responseWriter.Write(
547							@"HTTP/1.1 200 OK
548Accept-Ranges: bytes
549Connection: close
550Content-Type: text/html; charset=UTF-8
551
552<html><body>
553<h1>" +ThisIsNotAWebserverMessage + @"</h1>
554</body></html>");
555						
556						// Begin sending the data to the remote device.
557						data.Socket.BeginSend(response.ToArray(), 0,
558							(int)response.Length, SocketFlags.None, SendCallback,
559							data.Socket);
560						data.Dispose();
561						return;
562					}
563				}
564
565				messageLength--;
566				// If this is a compressed message, read another byte!
567				bool isCompressed = messageType == CompressionIndicatorByteValue;
568				// Read the message type again (if possible), no need to warn if this
569				// is not possible, restDataLength will be smaller than messageLength
570				if (isCompressed &&
571				    memStream.Position < memStream.Length)
572				{
573					messageType = reader.ReadByte();
574					messageLength--;
575				}
576
577				if (messageLength < 0 ||
578				    messageLength > MaxMessageLength)
579				{
580					Log.Warning(
581						"Invalid message length for message " + messageType +
582						": Need " + messageLength + " bytes of data (maximum is " +
583						MaxMessageLength + "), got " + remainingDataLength + " bytes " +
584						"right now. Aborting! Warnings will only stop if the next " +
585						"package starts with a new message (hopefully it often does).",
586						false);
587					break;
588				}
589
590				int restDataLength = (int)(memStream.Length - memStream.Position);
591
592
593				if (restDataLength < messageLength)
594				{
595					// Send out preCheckMessage events (usually unused)
596					data.HandlePreCheckMessage(messageType,
597						restDataLength / (float)messageLength);
598
599					// Remember data and start over when receiving the next packet!
600					data.rememberLastData = new byte[remainingDataLength];
601					memStream.Seek(remRestDataStartPos, SeekOrigin.Begin);
602					int bytesRead = memStream.Read(data.rememberLastData, 0,
603						remainingDataLength);
604					// And quit, we need to wait for more data
605					return;
606				}
607
608				// Note: Create a new memory stream here again. This is not having the
609				// best performance as we could use the existing stream, but we want
610				// to make 100% sure that all data is exactly read (not more or less).
611				MemoryStream restDataStream;
612
613				// Note: restData can be null for messages without data!
614				if (messageLength > 0)
615				{
616					byte[] restData = reader.ReadBytes(messageLength);
617					restDataStream = new MemoryStream(restData);
618					if (restData.Length != messageLength)
619					{
620						Log.Warning(
621							"Unable to read " + messageLength + " bytes for " +
622							"message " + messageType + ". Only " + restData.Length +
623							" bytes were actually read!");
624					}
625				}
626				else
627				{
628					// Create empty memory stream just to make the handle messaging
629					// easier (no need for null checking).
630					restDataStream = new MemoryStream();
631				}
632
633				// Check if this message type is valid. Note: Done at end to make sure
634				// we have read all data so the next message works again.
635				if (messageType < data.maxNumOfMessageTypes)
636				{
637					// And finally send out the messageReceived event!
638					data.OnRawMessageReceived(messageType,
639						new BinaryReader(restDataStream), isCompressed);
640				}
641				else
642				{
643					// Don't put stuff in rememberLastData!
644					Log.Warning(
645						"Invalid network message type=" + messageType +
646						" (max=" + data.maxNumOfMessageTypes + "), isCompressed=" +
647						isCompressed + ", messageLength=" + messageLength +
648						"), ignoring message and its data!");
649				}
650
651				// And kill the data stream again.
652				if (restDataStream != null)
653				{
654					restDataStream.Dispose();
655				}
656
657				// Continue as long there is more data to process (multiple messages)
658			} while (memStream.Position < memStream.Length);
659		}
660		#endregion
661
662		#region SendCallback
663		/// <summary>
664		/// Call back method to handle outgoing data, used for SendMessageData
665		/// </summary>
666		/// <param name="ar">Asynchronous state, which holds the socket</param>
667		private static void SendCallback(IAsyncResult ar)
668		{
669			// Retrieve the socket from the async state object.
670			Socket handler = (Socket)ar.AsyncState;
671			try
672			{
673				// Complete sending the data to the remote device.
674				int bytesSent = handler.EndSend(ar);
675			}
676			catch (ObjectDisposedException)
677			{
678				// ignore, happens when sockets gets killed!
679			}
680			catch (SocketException)
681			{
682				// Ignore, can happen is socket was closed abruptly
683			}
684			catch (Exception ex)
685			{
686				Log.Warning("SendCallback failed while sending data: " +
687				            ex);
688			}
689		}
690		#endregion
691
692		#endregion
693	}
694}