/Utilities/Networking/SocketHelper.cs
C# | 694 lines | 433 code | 53 blank | 208 comment | 67 complexity | f89627692197366f682a4459a9b222fb MD5 | raw file
Possible License(s): Apache-2.0
- // Warning: When LOG_STUFF is on and LogServer is used, we could get in trouble
- //#define LOG_STUFF
-
- using System;
- using System.Diagnostics;
- using System.IO;
- using System.Net;
- using System.Net.Sockets;
- using System.Threading;
- using Delta.Utilities.Compression;
- using Delta.Utilities.Cryptography;
- using Delta.Utilities.Helpers;
-
- namespace Delta.Utilities.Networking
- {
- /// <summary>
- /// SocketHelper class, stuff used wherever network stuff is needed!
- /// </summary>
- public static class SocketHelper
- {
- #region Constants
- /// <summary>
- /// Receive up to 8 low level packets with 1440 (1500 is MTU -40 bytes for
- /// TCP/IP -20 bytes for different needs (e.g. VPN uses some bytes)) data.
- /// Bigger messages than this will be put together inside the
- /// OnReceivedDataAsyncCallback method with multiple calls to Receive.
- /// Most messages will be way smaller (few bytes).
- /// </summary>
- internal const int ReceiveBufferSize = 1440 * 8; //128;//8192;
-
- /// <summary>
- /// Maximum message size for one packet should not go beyond 128 MB!
- /// Usually there is something wrong if we get messages this big.
- /// </summary>
- private const int MaxMessageLength = 1024 * 1024 * 128;
-
- /// <summary>
- /// Indicator for compressed messages (done before encryption). This will
- /// be saved as an extra byte right before the message type (this message
- /// type number is not allowed).
- /// </summary>
- private const byte CompressionIndicatorByteValue = 255;
-
- /// <summary>
- /// Message that will be displayed when a browser tries to connect to this
- /// service! The connection will be closed.
- /// </summary>
- public static string ThisIsNotAWebserverMessage =
- "This is not a webserver, unable to continue. Please use the correct " +
- "tool to connect to this service!";
- #endregion
-
- #region OnReceivedDataAsyncCallback (Static)
- /// <summary>
- /// On received data async callback. Public to allow overridden
- /// ClientData.StartReceivingMessagesAndSendConnectMessage to use this.
- /// </summary>
- /// <param name="ar">AsyncState, which holds the BaseClient</param>
- public static void OnReceivedDataAsyncCallback(IAsyncResult ar)
- {
- BaseClient data = null;
- try
- {
- data = (BaseClient)ar.AsyncState;
- if (data == null ||
- data.Socket == null ||
- data.Socket.Connected == false)
- {
- // We got no socket to receive anything, abort!
- return;
- }
-
- // Make sure we handle one received data block at a time!
- lock (data)
- {
- int numOfReceivedBytes = data.Socket.EndReceive(ar);
- if (numOfReceivedBytes == 0)
- {
- // Nothing received, close connection. This is normal for
- // disconnecting sockets in TCP/IP
- if (data.OnDisconnected != null)
- {
- data.OnDisconnected();
- }
- data.Dispose();
- return;
- }
-
- // Otherwise everything is fine, handle the received data and
- // fire the messageReceived event for each received message.
- try
- {
- ReceiveMessageData(data, numOfReceivedBytes);
- }
- catch (SocketException)
- {
- // Note: OnLogin will be called to notify the caller
- data.Dispose();
- if (data.OnDisconnected != null)
- {
- data.OnDisconnected();
- }
- }
- catch (Exception ex)
- {
- Log.Warning("ReceiveMessageData failed: " + ex);
- }
-
- // And finally setup the receive callback again if we are still
- // connected (there might have been a forced disconnect)!
- if (data.Socket != null)
- {
- SetupReceiveCallback(data.Socket, data.clientBufferData,
- OnReceivedDataAsyncCallback, data);
- }
- }
- }
- catch (SocketException)
- {
- // Disconnect client, this happened: SocketException (0x80004005):
- // An existing connection was forcibly closed by the remote host
- if (data != null)
- {
- // Note: OnLogin will be called to notify the caller if this happened
- // before a connection was established, and we also call
- // OnDisconnected in case the user wants to know when this happened.
- try
- {
- data.Dispose();
- if (data.OnDisconnected != null)
- {
- data.OnDisconnected();
- }
- }
- catch (Exception ex)
- {
- Log.Warning("OnReceivedData failed to dispose client: " + ex);
- }
- }
- }
- catch (Exception ex)
- {
- Log.Warning("OnReceivedData failed: " + ex);
- }
- }
- #endregion
-
-
- #region SendMessageBytes (Static)
- /// <summary>
- /// Send data to socket, will build packet with length of message, and then
- /// add the type and message data (if there is any). A message has a
- /// minimum size of 2 bytes (length and type, e.g. 1,10 indicates there is
- /// a message with 1 bytes: message type = 10 with no extra data).
- /// <para />
- /// This method does handle all the compression and optionally encryption
- /// logic that can be enabled on the Server and Client side. The receiving
- /// side is handled in <see cref="BaseClient.OnRawMessageReceived"/>.
- /// </summary>
- /// <param name="socket">Socket to send the data to</param>
- /// <param name="messageType">Type of message (as byte)</param>
- /// <param name="data">Data to send</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. An extra
- /// byte with 0xFF (255) is sent to mark that the incoming message is
- /// compressed.
- /// </param>
- /// <param name="encryptMessage">Encript this message?</param>
- public static void SendMessageBytes(Socket socket, byte messageType,
- byte[] data, bool allowToCompressMessage, AES encryptMessage = null)
- {
- // Not connected? Then simply don't send!
- if (socket == null ||
- socket.Connected == false)
- {
- return;
- }
-
- MemoryStream memStream = new MemoryStream();
- BinaryWriter writer = new BinaryWriter(memStream);
- // How big is this message? Including the messageType, so each
- // message is at least this number plus 1 byte of data length.
- int messageLength =
- 1 +
- (data != null
- ? data.Length
- : 0);
- int originalDataLength = 0;
- // Only compress messages above 1024 bytes of payload data!
- if (data != null &&
- // Do not compress or encrypt the connect message, we need it to
- // setup the encryption system and we cannot decrypt without the data!
- messageType != (byte)BasicMessageTypes.Connect)
- {
- // First handle compression (if the message is big enough)
- if (allowToCompressMessage &&
- data.Length >= Zip.MinimumByteDataLengthToZip)
- {
- // Compress the data, but only keep it if it is smaller
- byte[] compressedData = Zip.Compress(data);
- if (compressedData.Length < data.Length)
- {
- data = compressedData;
- // Add an extra byte with 0xFF (255) to mark as compressed!
- messageLength = 1 + 1 + data.Length;
- }
- else
- {
- // Disable compression, not worth it, keep existing message length
- allowToCompressMessage = false;
- }
- }
- else
- {
- // Otherwise no compression is used
- allowToCompressMessage = false;
- }
-
- // Next encrypt the message if enabled
- if (encryptMessage != null)
- {
- // Remember the original data length (already might be compressed)
- originalDataLength = data.Length;
- // Encrypt the data
- data = encryptMessage.Encrypt(data);
- // Now calculate the new amount of bytes we have to save (see below)
- // The length is the message type (1 byte), if compression is on
- // (1 byte), the original data length (1-7 bytes) and the encrypted
- // data (data.Length).
- messageLength =
- 1 +
- (allowToCompressMessage
- ? 1
- : 0) +
- StreamHelper.GetDataLengthOfNumberMostlySmallerThan254(
- originalDataLength) + data.Length;
- }
- }
- else
- {
- // Otherwise no compression is used
- allowToCompressMessage = false;
- }
-
- // Write out the total length of the message. We expect most messages
- // to be small (below 254 bytes), but even for very big messages the
- // overhead is only 1 byte to store int length values.
- StreamHelper.WriteNumberMostlySmallerThan254(writer, messageLength);
-
- // Mark this as compressed message to make sure we decompress it on
- // the receiver side (this will make the message 1 byte longer, but
- // we only do this for big messages and compression saves lots of bytes)
- if (allowToCompressMessage)
- {
- writer.Write(CompressionIndicatorByteValue);
- }
- // Next store the message type
- writer.Write(messageType);
-
- // If the data was encrypted above, save the original data length
- if (originalDataLength > 0)
- {
- StreamHelper.WriteNumberMostlySmallerThan254(writer,
- originalDataLength);
- }
-
- // And finally store the data itself
- if (data != null)
- {
- // Write out the data we got as input here (already optionally
- // compressed and encrypted, see above).
- writer.Write(data);
- }
-
- try
- {
- // Begin sending the data to the remote device.
- socket.BeginSend(memStream.GetBuffer(), 0, (int)memStream.Length,
- SocketFlags.None, SendCallback, socket);
- }
- catch
- {
- //don't throw error or log it, just ignore (happens when closing)!
- }
-
- // We don't need the memory stream anymore, it can safely be disposed
- memStream.Dispose();
- }
- #endregion
-
- #region ConnectTcpSocket (Static)
- /// <summary>
- /// Helper method to simply connect a socket to a specific server
- /// by the given address and port number. Used mainly for unit tests, but
- /// thanks to the timeout parameter also really useful in applications!
- /// </summary>
- /// <param name="address">The server address</param>
- /// <param name="port">The servers port number</param>
- /// <returns>The connected socket or null if anything failed (timeout or
- /// some other failure, which will be logged btw).</returns>
- public static Socket ConnectTcpSocket(string address, int port)
- {
- int timeoutMs = 750; //666//500
- return ConnectTcpSocket(address, new[]
- {
- port
- }, timeoutMs, true);
- }
-
- /// <summary>
- /// Connect a socket to the specified address and port and abort if
- /// the specified timeout exceeded.
- /// </summary>
- /// <param name="address">
- /// The IP or DNS name of the host we want to connect to.
- /// </param>
- /// <param name="ports">
- /// The port numbers of the host, usually just a single port.
- /// </param>
- /// <param name="timeoutMs">The timeout in milliseconds after which
- /// we abort the connecting process if nothing happened yet.</param>
- /// <param name="warnIfConnectFailed">Warn if the connection failed,
- /// which is on by default, but sometimes we don't care.</param>
- /// <returns></returns>
- public static Socket ConnectTcpSocket(string address, int[] ports,
- int timeoutMs, bool warnIfConnectFailed)
- {
- if (ports == null ||
- ports.Length == 0)
- {
- throw new ArgumentNullException(
- "Unable to connect to tcp socket at '" + address +
- "' without given port!");
- }
-
- try
- {
- LastConnectionError = "";
-
- // Now create end point for connecting for each specified port until
- // we could successfully connect.
- foreach (int port in ports)
- {
- DnsEndPoint logServerIp = new DnsEndPoint(address, port);
- // Create our socket
- Socket serverSocket = new Socket(AddressFamily.InterNetwork,
- SocketType.Stream, ProtocolType.Tcp);
-
- // And finally connect to the server using a timeout (0.5 seconds)
- IAsyncResult result = serverSocket.BeginConnect(logServerIp,
- null, null);
- bool success = result.AsyncWaitHandle.WaitOne(timeoutMs, true);
- if (success == false ||
- serverSocket.Connected == false)
- {
- // Failed to connect server in given timeout!
- serverSocket.Close();
- }
- // Else we had success
- else
- {
- serverSocket.EndConnect(result);
- return serverSocket;
- }
- } // foreach
-
- // All connection attempts failed, return null
- return null;
- }
- catch (ThreadAbortException)
- {
- // Re-throw, caller must handle this (might want to abort other stuff)
- throw;
- }
- catch (Exception ex)
- {
- LastConnectionError = ex.Message;
- if (warnIfConnectFailed)
- {
- Log.Warning("Failed to connect to tcp server (" + address + ":" +
- ports.Write() + "): " + ex);
- }
- return null;
- }
- }
- #endregion
-
-
- #region WriteClientInfo (Static)
- /// <summary>
- /// Write client info helper to get more details about a client socket.
- /// This is basically the RemoteEndPoint (IP and port) if available,
- /// otherwise "<null>" or an error is returned.
- /// </summary>
- public static string WriteClientInfo(Socket client)
- {
- if (client == null ||
- client.Connected == false)
- {
- return "<Null>";
- }
-
- try
- {
- return client.RemoteEndPoint.ToString();
- }
- catch (Exception ex)
- {
- return "Unable to return WriteClientInfo: " + ex;
- }
- }
- #endregion
-
- #region GetIP (Static)
- /// <summary>
- /// Basically the same as WriteClient, but will just return the remote IP
- /// without the used remote port.
- /// </summary>
- public static string GetIP(Socket client)
- {
- if (client == null ||
- client.Connected == false)
- {
- return "<Null>";
- }
-
- try
- {
- return (client.RemoteEndPoint as IPEndPoint).Address.ToString();
- }
- catch (Exception ex)
- {
- return "GetIP failed: " + ex.Message;
- }
- }
- #endregion
-
- #region LastConnectionError (Static)
- /// <summary>
- /// Last connection error we got in ConnectTcpSocket (if we got any).
- /// </summary>
- public static string LastConnectionError = "";
- #endregion
-
- #region Methods (Private)
-
- #region SetupReceiveCallback
- /// <summary>
- /// Setup the receive callback stuff. Used to initially setup the
- /// receive callback and then every time in GetReceivedData!
- /// </summary>
- /// <param name="clientSocket">Client socket for receiving data</param>
- /// <param name="buffer">Client buffer for the data to receive</param>
- /// <param name="receiveCallback">
- /// Receive callback, which is executed once we receive some data on this
- /// socket.
- /// </param>
- /// <param name="obj">
- /// Object for custom user data (usually BaseClient).
- /// </param>
- /// <returns>True if we received something, false otherwise</returns>
- internal static bool SetupReceiveCallback(Socket clientSocket,
- byte[] buffer, AsyncCallback receiveCallback, object obj)
- {
- if (clientSocket == null ||
- clientSocket.Connected == false)
- {
- return false;
- }
- clientSocket.BeginReceive(buffer, 0, buffer.Length,
- SocketFlags.None, receiveCallback, obj);
- return true;
- }
- #endregion
-
-
- #region ReceiveMessageData
- /// <summary>
- /// Handle all the receive data issues, extract the message length and
- /// determines if we need more data and will wait for next call
- /// in this case. Will also read type of message and check if its
- /// valid. You can use preCheckMessage to perform some operations while
- /// message is still not complete. Finally use handleMessage to
- /// perform the message action, etc.
- /// </summary>
- /// <param name="data">Client for data to be received</param>
- /// <param name="numOfReceivedBytes">num of received bytes</param>
- private static void ReceiveMessageData(BaseClient data,
- int numOfReceivedBytes)
- {
- // Build memory stream from rememberLastData and the current received
- // data in clientBufferData, but only use numOfReceivedBytes.
- MemoryStream memStream = new MemoryStream();
- // Was there anything remember from last time?
- if (data.rememberLastData != null)
- {
- memStream.Write(data.rememberLastData, 0,
- data.rememberLastData.Length);
- data.rememberLastData = null;
- }
- // And write how much data we received just now
- memStream.Write(data.clientBufferData, 0, numOfReceivedBytes);
-
- // Finally reset the stream position and read the data back
- memStream.Seek(0, SeekOrigin.Begin);
- BinaryReader reader = new BinaryReader(memStream);
- // Loop until we have all data processed (might be several messages)
- do
- {
- // Remember memStreamPos if we want to remember data for later use!
- int remRestDataStartPos = (int)memStream.Position;
- int remainingDataLength = (int)(memStream.Length - memStream.Position);
-
- // Can't continue without at least the length of message
- int messageLength;
- if (StreamHelper.TryToGetNumberMostlySmallerThan254(reader,
- out messageLength) == false ||
- // We need one more byte to read the message type!
- memStream.Length - memStream.Position < 1)
- {
- // Remember data, try again when more data has arrived
- data.rememberLastData = new byte[remainingDataLength];
- memStream.Seek(remRestDataStartPos, SeekOrigin.Begin);
- int bytesRead = memStream.Read(data.rememberLastData, 0,
- remainingDataLength);
- // Wait until more data is available!
- return;
- }
-
- // Read the message type and reduce messageLength by one.
- byte messageType = reader.ReadByte();
-
- // Is this a Http request? E.g.
- // GET /index.html HTTP/1.1 ...
- if (messageLength == (byte)'G' &&
- messageType == (byte)'E')
- {
- char nextChar = (char)reader.PeekChar();
- if (nextChar == 'T')
- {
- // Then abort and send a dummy webpage back to the client
- // and disconnect him, this is not a HTTP server!
- MemoryStream response = new MemoryStream();
- BinaryWriter responseWriter = new BinaryWriter(response);
- responseWriter.Write(
- @"HTTP/1.1 200 OK
- Accept-Ranges: bytes
- Connection: close
- Content-Type: text/html; charset=UTF-8
-
- <html><body>
- <h1>" +ThisIsNotAWebserverMessage + @"</h1>
- </body></html>");
-
- // Begin sending the data to the remote device.
- data.Socket.BeginSend(response.ToArray(), 0,
- (int)response.Length, SocketFlags.None, SendCallback,
- data.Socket);
- data.Dispose();
- return;
- }
- }
-
- messageLength--;
- // If this is a compressed message, read another byte!
- bool isCompressed = messageType == CompressionIndicatorByteValue;
- // Read the message type again (if possible), no need to warn if this
- // is not possible, restDataLength will be smaller than messageLength
- if (isCompressed &&
- memStream.Position < memStream.Length)
- {
- messageType = reader.ReadByte();
- messageLength--;
- }
-
- if (messageLength < 0 ||
- messageLength > MaxMessageLength)
- {
- Log.Warning(
- "Invalid message length for message " + messageType +
- ": Need " + messageLength + " bytes of data (maximum is " +
- MaxMessageLength + "), got " + remainingDataLength + " bytes " +
- "right now. Aborting! Warnings will only stop if the next " +
- "package starts with a new message (hopefully it often does).",
- false);
- break;
- }
-
- int restDataLength = (int)(memStream.Length - memStream.Position);
-
-
- if (restDataLength < messageLength)
- {
- // Send out preCheckMessage events (usually unused)
- data.HandlePreCheckMessage(messageType,
- restDataLength / (float)messageLength);
-
- // Remember data and start over when receiving the next packet!
- data.rememberLastData = new byte[remainingDataLength];
- memStream.Seek(remRestDataStartPos, SeekOrigin.Begin);
- int bytesRead = memStream.Read(data.rememberLastData, 0,
- remainingDataLength);
- // And quit, we need to wait for more data
- return;
- }
-
- // Note: Create a new memory stream here again. This is not having the
- // best performance as we could use the existing stream, but we want
- // to make 100% sure that all data is exactly read (not more or less).
- MemoryStream restDataStream;
-
- // Note: restData can be null for messages without data!
- if (messageLength > 0)
- {
- byte[] restData = reader.ReadBytes(messageLength);
- restDataStream = new MemoryStream(restData);
- if (restData.Length != messageLength)
- {
- Log.Warning(
- "Unable to read " + messageLength + " bytes for " +
- "message " + messageType + ". Only " + restData.Length +
- " bytes were actually read!");
- }
- }
- else
- {
- // Create empty memory stream just to make the handle messaging
- // easier (no need for null checking).
- restDataStream = new MemoryStream();
- }
-
- // Check if this message type is valid. Note: Done at end to make sure
- // we have read all data so the next message works again.
- if (messageType < data.maxNumOfMessageTypes)
- {
- // And finally send out the messageReceived event!
- data.OnRawMessageReceived(messageType,
- new BinaryReader(restDataStream), isCompressed);
- }
- else
- {
- // Don't put stuff in rememberLastData!
- Log.Warning(
- "Invalid network message type=" + messageType +
- " (max=" + data.maxNumOfMessageTypes + "), isCompressed=" +
- isCompressed + ", messageLength=" + messageLength +
- "), ignoring message and its data!");
- }
-
- // And kill the data stream again.
- if (restDataStream != null)
- {
- restDataStream.Dispose();
- }
-
- // Continue as long there is more data to process (multiple messages)
- } while (memStream.Position < memStream.Length);
- }
- #endregion
-
- #region SendCallback
- /// <summary>
- /// Call back method to handle outgoing data, used for SendMessageData
- /// </summary>
- /// <param name="ar">Asynchronous state, which holds the socket</param>
- private static void SendCallback(IAsyncResult ar)
- {
- // Retrieve the socket from the async state object.
- Socket handler = (Socket)ar.AsyncState;
- try
- {
- // Complete sending the data to the remote device.
- int bytesSent = handler.EndSend(ar);
- }
- catch (ObjectDisposedException)
- {
- // ignore, happens when sockets gets killed!
- }
- catch (SocketException)
- {
- // Ignore, can happen is socket was closed abruptly
- }
- catch (Exception ex)
- {
- Log.Warning("SendCallback failed while sending data: " +
- ex);
- }
- }
- #endregion
-
- #endregion
- }
- }