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