PerMessageCompression.cs 15 KB

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