PerMessageCompression.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. #if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
  2. using System;
  3. using BestHTTP.Extensions;
  4. using BestHTTP.WebSocket.Frames;
  5. using BestHTTP.Decompression.Zlib;
  6. namespace BestHTTP.WebSocket.Extensions
  7. {
  8. /// <summary>
  9. /// Compression Extensions for WebSocket implementation.
  10. /// http://tools.ietf.org/html/rfc7692
  11. /// </summary>
  12. public sealed class PerMessageCompression : IExtension
  13. {
  14. private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF };
  15. #region Public Properties
  16. /// <summary>
  17. /// By including this extension parameter in an extension negotiation offer, a client informs the peer server
  18. /// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in
  19. /// the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
  20. /// </summary>
  21. public bool ClientNoContextTakeover { get; private set; }
  22. /// <summary>
  23. /// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
  24. /// </summary>
  25. public bool ServerNoContextTakeover { get; private set; }
  26. /// <summary>
  27. /// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
  28. /// </summary>
  29. public int ClientMaxWindowBits { get; private set; }
  30. /// <summary>
  31. /// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
  32. /// </summary>
  33. public int ServerMaxWindowBits { get; private set; }
  34. /// <summary>
  35. /// The compression level that the client will use to compress the frames.
  36. /// </summary>
  37. public CompressionLevel Level { get; private set; }
  38. /// <summary>
  39. /// What minimum data length will trigger the compression.
  40. /// </summary>
  41. public int MinimumDataLegthToCompress { get; set; }
  42. #endregion
  43. #region Private fields
  44. /// <summary>
  45. /// Cached object to support context takeover.
  46. /// </summary>
  47. private System.IO.MemoryStream compressorOutputStream;
  48. private DeflateStream compressorDeflateStream;
  49. /// <summary>
  50. /// Cached object to support context takeover.
  51. /// </summary>
  52. private System.IO.MemoryStream decompressorInputStream;
  53. private System.IO.MemoryStream decompressorOutputStream;
  54. private DeflateStream decompressorDeflateStream;
  55. private byte[] copyBuffer = new byte[1024];
  56. #endregion
  57. public PerMessageCompression()
  58. :this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, 10)
  59. { }
  60. public PerMessageCompression(CompressionLevel level,
  61. bool clientNoContextTakeover,
  62. bool serverNoContextTakeover,
  63. int desiredClientMaxWindowBits,
  64. int desiredServerMaxWindowBits,
  65. int minDatalengthToCompress)
  66. {
  67. this.Level = level;
  68. this.ClientNoContextTakeover = clientNoContextTakeover;
  69. this.ServerNoContextTakeover = ServerNoContextTakeover;
  70. this.ClientMaxWindowBits = desiredClientMaxWindowBits;
  71. this.ServerMaxWindowBits = desiredServerMaxWindowBits;
  72. this.MinimumDataLegthToCompress = minDatalengthToCompress;
  73. }
  74. #region IExtension Implementation
  75. /// <summary>
  76. /// This will start the permessage-deflate negotiation process.
  77. /// <seealso href="http://tools.ietf.org/html/rfc7692#section-5.1"/>
  78. /// </summary>
  79. public void AddNegotiation(HTTPRequest request)
  80. {
  81. // The default header value that we will send out minimum.
  82. string headerValue = "permessage-deflate";
  83. // http://tools.ietf.org/html/rfc7692#section-7.1.1.1
  84. // A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value.
  85. // By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover.
  86. // If the peer server doesn't use context takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages.
  87. if (this.ServerNoContextTakeover)
  88. headerValue += "; server_no_context_takeover";
  89. // http://tools.ietf.org/html/rfc7692#section-7.1.1.2
  90. // A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer.
  91. // This extension parameter has no value. By including this extension parameter in an extension negotiation offer,
  92. // a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover"
  93. // extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover.
  94. if (this.ClientNoContextTakeover)
  95. headerValue += "; client_no_context_takeover";
  96. // http://tools.ietf.org/html/rfc7692#section-7.1.2.1
  97. // By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server
  98. // will use to compress messages.If the peer server uses a small LZ77 sliding window to compress messages, the client can reduce the memory needed for the LZ77 sliding window.
  99. if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax)
  100. headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString();
  101. else
  102. // Absence of this parameter in an extension negotiation offer indicates that the client can receive messages compressed using an LZ77 sliding window of up to 32,768 bytes.
  103. this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax;
  104. // http://tools.ietf.org/html/rfc7692#section-7.1.2.2
  105. // By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits"
  106. // extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter.
  107. if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax)
  108. headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString();
  109. else
  110. {
  111. headerValue += "; client_max_window_bits";
  112. // If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the
  113. // peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding
  114. // extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include
  115. // the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified
  116. // by the value in the extension negotiation offer to compress messages.
  117. this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax;
  118. }
  119. // Add the new header to the request.
  120. request.AddHeader("Sec-WebSocket-Extensions", headerValue);
  121. }
  122. public bool ParseNegotiation(WebSocketResponse resp)
  123. {
  124. // Search for any returned neogitation offer
  125. var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions");
  126. if (headerValues == null)
  127. return false;
  128. for (int i = 0; i < headerValues.Count; ++i)
  129. {
  130. // If found, tokenize it
  131. HeaderParser parser = new HeaderParser(headerValues[i]);
  132. for (int cv = 0; cv < parser.Values.Count; ++cv)
  133. {
  134. HeaderValue value = parser.Values[i];
  135. if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase))
  136. {
  137. HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]);
  138. HeaderValue option;
  139. if (value.TryGetOption("client_no_context_takeover", out option))
  140. this.ClientNoContextTakeover = true;
  141. if (value.TryGetOption("server_no_context_takeover", out option))
  142. this.ServerNoContextTakeover = true;
  143. if (value.TryGetOption("client_max_window_bits", out option))
  144. if (option.HasValue)
  145. {
  146. int windowBits;
  147. if (int.TryParse(option.Value, out windowBits))
  148. this.ClientMaxWindowBits = windowBits;
  149. }
  150. if (value.TryGetOption("server_max_window_bits", out option))
  151. if (option.HasValue)
  152. {
  153. int windowBits;
  154. if (int.TryParse(option.Value, out windowBits))
  155. this.ServerMaxWindowBits = windowBits;
  156. }
  157. return true;
  158. }
  159. }
  160. }
  161. return false;
  162. }
  163. /// <summary>
  164. /// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data
  165. /// in the writer.
  166. /// </summary>
  167. public byte GetFrameHeader(WebSocketFrame writer, byte inFlag)
  168. {
  169. // http://tools.ietf.org/html/rfc7692#section-7.2.3.1
  170. // the RSV1 bit is set only on the first frame.
  171. if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) &&
  172. writer.Data != null && writer.Data.Length >= this.MinimumDataLegthToCompress)
  173. return (byte)(inFlag | 0x40);
  174. else
  175. return inFlag;
  176. }
  177. /// <summary>
  178. /// IExtension implementation to be able to compress the data hold in the writer.
  179. /// </summary>
  180. public byte[] Encode(WebSocketFrame writer)
  181. {
  182. if (writer.Data == null)
  183. return WebSocketFrame.NoData;
  184. // Is compressing enabled for this frame? If so, compress it.
  185. if ((writer.Header & 0x40) != 0)
  186. return Compress(writer.Data);
  187. else
  188. return writer.Data;
  189. }
  190. /// <summary>
  191. /// IExtension implementation to possible decompress the data.
  192. /// </summary>
  193. public byte[] Decode(byte header, byte[] data)
  194. {
  195. // Is the server compressed the data? If so, decompress it.
  196. if ((header & 0x40) != 0)
  197. return Decompress(data);
  198. else
  199. return data;
  200. }
  201. #endregion
  202. #region Private Helper Functions
  203. /// <summary>
  204. /// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream).
  205. /// </summary>
  206. private byte[] Compress(byte[] data)
  207. {
  208. if (compressorOutputStream == null)
  209. compressorOutputStream = new System.IO.MemoryStream();
  210. compressorOutputStream.SetLength(0);
  211. if (compressorDeflateStream == null)
  212. {
  213. compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, true, this.ClientMaxWindowBits);
  214. compressorDeflateStream.FlushMode = FlushType.Sync;
  215. }
  216. byte[] result = null;
  217. try
  218. {
  219. compressorDeflateStream.Write(data, 0, data.Length);
  220. compressorDeflateStream.Flush();
  221. compressorOutputStream.Position = 0;
  222. // http://tools.ietf.org/html/rfc7692#section-7.2.1
  223. // Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. After this step, the last octet of the compressed data contains (possibly part of) the DEFLATE header bits with the "BTYPE" bits set to 00.
  224. compressorOutputStream.SetLength(compressorOutputStream.Length - 4);
  225. result = compressorOutputStream.ToArray();
  226. }
  227. finally
  228. {
  229. if (this.ClientNoContextTakeover)
  230. {
  231. compressorDeflateStream.Dispose();
  232. compressorDeflateStream = null;
  233. }
  234. }
  235. return result;
  236. }
  237. /// <summary>
  238. /// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream).
  239. /// </summary>
  240. private byte[] Decompress(byte[] data)
  241. {
  242. if (decompressorInputStream == null)
  243. decompressorInputStream = new System.IO.MemoryStream(data.Length + 4);
  244. decompressorInputStream.Write(data, 0, data.Length);
  245. // http://tools.ietf.org/html/rfc7692#section-7.2.2
  246. // Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message.
  247. decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length);
  248. decompressorInputStream.Position = 0;
  249. if (decompressorDeflateStream == null)
  250. {
  251. decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, true, this.ServerMaxWindowBits);
  252. decompressorDeflateStream.FlushMode = FlushType.Sync;
  253. }
  254. if (decompressorOutputStream == null)
  255. decompressorOutputStream = new System.IO.MemoryStream();
  256. decompressorOutputStream.SetLength(0);
  257. int readCount;
  258. while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
  259. decompressorOutputStream.Write(copyBuffer, 0, readCount);
  260. decompressorDeflateStream.SetLength(0);
  261. byte[] result = decompressorOutputStream.ToArray();
  262. if (this.ServerNoContextTakeover)
  263. {
  264. decompressorDeflateStream.Dispose();
  265. decompressorDeflateStream = null;
  266. }
  267. return result;
  268. }
  269. #endregion
  270. }
  271. }
  272. #endif