#if !UNITY_WEBGL || UNITY_EDITOR using System; using Best.HTTP.Shared; using Best.HTTP.Shared.Compression.Zlib; using Best.HTTP.Shared.Extensions; using Best.HTTP.Shared.PlatformSupport.Memory; using Best.HTTP.Shared.Streams; using Best.WebSockets.Implementations.Frames; namespace Best.WebSockets.Extensions { /// /// Compression Extensions for WebSocket implementation. /// http://tools.ietf.org/html/rfc7692 /// public sealed class PerMessageCompression : IExtension { public const int MinDataLengthToCompressDefault = 256; private static readonly byte[] Trailer = new byte[] { 0x00, 0x00, 0xFF, 0xFF }; #region Public Properties /// /// By including this extension parameter in an extension negotiation offer, a client informs the peer server /// of a hint that even if the server doesn't include the "client_no_context_takeover" extension parameter in /// the corresponding extension negotiation response to the offer, the client is not going to use context takeover. /// public bool ClientNoContextTakeover { get; private set; } /// /// By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover. /// public bool ServerNoContextTakeover { get; private set; } /// /// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context. /// public int ClientMaxWindowBits { get; private set; } /// /// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context. /// public int ServerMaxWindowBits { get; private set; } /// /// The compression level that the client will use to compress the frames. /// public CompressionLevel Level { get; private set; } /// /// What minimum data length will trigger the compression. /// public int MinimumDataLegthToCompress { get; set; } #endregion #region Private fields /// /// Cached object to support context takeover. /// private BufferPoolMemoryStream compressorOutputStream; private DeflateStream compressorDeflateStream; /// /// Cached object to support context takeover. /// private BufferPoolMemoryStream decompressorInputStream; private BufferPoolMemoryStream decompressorOutputStream; private DeflateStream decompressorDeflateStream; #endregion public PerMessageCompression() :this(CompressionLevel.Default, false, false, ZlibConstants.WindowBitsMax, ZlibConstants.WindowBitsMax, MinDataLengthToCompressDefault) { } public PerMessageCompression(CompressionLevel level, bool clientNoContextTakeover, bool serverNoContextTakeover, int desiredClientMaxWindowBits, int desiredServerMaxWindowBits, int minDatalengthToCompress) { this.Level = level; this.ClientNoContextTakeover = clientNoContextTakeover; this.ServerNoContextTakeover = serverNoContextTakeover; this.ClientMaxWindowBits = desiredClientMaxWindowBits; this.ServerMaxWindowBits = desiredServerMaxWindowBits; this.MinimumDataLegthToCompress = minDatalengthToCompress; } #region IExtension Implementation /// /// This will start the permessage-deflate negotiation process. /// /// public void AddNegotiation(HTTP.HTTPRequest request) { // The default header value that we will send out minimum. string headerValue = "permessage-deflate"; // http://tools.ietf.org/html/rfc7692#section-7.1.1.1 // A client MAY include the "server_no_context_takeover" extension parameter in an extension negotiation offer. This extension parameter has no value. // By including this extension parameter in an extension negotiation offer, a client prevents the peer server from using context takeover. // 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. if (this.ServerNoContextTakeover) headerValue += "; server_no_context_takeover"; // http://tools.ietf.org/html/rfc7692#section-7.1.1.2 // A client MAY include the "client_no_context_takeover" extension parameter in an extension negotiation offer. // This extension parameter has no value. By including this extension parameter in an extension negotiation offer, // a client informs the peer server of a hint that even if the server doesn't include the "client_no_context_takeover" // extension parameter in the corresponding extension negotiation response to the offer, the client is not going to use context takeover. if (this.ClientNoContextTakeover) headerValue += "; client_no_context_takeover"; // http://tools.ietf.org/html/rfc7692#section-7.1.2.1 // By including this parameter in an extension negotiation offer, a client limits the LZ77 sliding window size that the server // 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. if (this.ServerMaxWindowBits != ZlibConstants.WindowBitsMax) headerValue += "; server_max_window_bits=" + this.ServerMaxWindowBits.ToString(); else // 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. this.ServerMaxWindowBits = ZlibConstants.WindowBitsMax; // http://tools.ietf.org/html/rfc7692#section-7.1.2.2 // By including this parameter in an offer, a client informs the peer server that the client supports the "client_max_window_bits" // extension parameter in an extension negotiation response and, optionally, a hint by attaching a value to the parameter. if (this.ClientMaxWindowBits != ZlibConstants.WindowBitsMax) headerValue += "; client_max_window_bits=" + this.ClientMaxWindowBits.ToString(); else { headerValue += "; client_max_window_bits"; // If the "client_max_window_bits" extension parameter in an extension negotiation offer has a value, the parameter also informs the // peer server of a hint that even if the server doesn't include the "client_max_window_bits" extension parameter in the corresponding // extension negotiation response with a value greater than the one in the extension negotiation offer or if the server doesn't include // the extension parameter at all, the client is not going to use an LZ77 sliding window size greater than the size specified // by the value in the extension negotiation offer to compress messages. this.ClientMaxWindowBits = ZlibConstants.WindowBitsMax; } // Add the new header to the request. request.AddHeader("Sec-WebSocket-Extensions", headerValue); } public bool ParseNegotiation(HTTP.HTTPResponse resp) { // Search for any returned neogitation offer var headerValues = resp.GetHeaderValues("Sec-WebSocket-Extensions"); if (headerValues == null) return false; for (int i = 0; i < headerValues.Count; ++i) { // If found, tokenize it HeaderParser parser = new HeaderParser(headerValues[i]); for (int cv = 0; cv < parser.Values.Count; ++cv) { HeaderValue value = parser.Values[i]; if (!string.IsNullOrEmpty(value.Key) && value.Key.StartsWith("permessage-deflate", StringComparison.OrdinalIgnoreCase)) { HTTPManager.Logger.Information("PerMessageCompression", "Enabled with header: " + headerValues[i]); HeaderValue option; if (value.TryGetOption("client_no_context_takeover", out option)) this.ClientNoContextTakeover = true; if (value.TryGetOption("server_no_context_takeover", out option)) this.ServerNoContextTakeover = true; if (value.TryGetOption("client_max_window_bits", out option)) if (option.HasValue) { int windowBits; if (int.TryParse(option.Value, out windowBits)) this.ClientMaxWindowBits = windowBits; } if (value.TryGetOption("server_max_window_bits", out option)) if (option.HasValue) { int windowBits; if (int.TryParse(option.Value, out windowBits)) this.ServerMaxWindowBits = windowBits; } return true; } } } return false; } /// /// IExtension implementation to set the Rsv1 flag in the header if we are we will want to compress the data /// in the writer. /// public byte GetFrameHeader(WebSocketFrame writer, byte inFlag) { // http://tools.ietf.org/html/rfc7692#section-7.2.3.1 // the RSV1 bit is set only on the first frame. if ((writer.Type == WebSocketFrameTypes.Binary || writer.Type == WebSocketFrameTypes.Text) && writer.Data != null && writer.Data.Count >= this.MinimumDataLegthToCompress) return (byte)(inFlag | 0x40); else return inFlag; } /// /// IExtension implementation to be able to compress the data hold in the writer. /// public BufferSegment Encode(WebSocketFrame writer) { if (writer.Data == null) return BufferSegment.Empty; // Is compressing enabled for this frame? If so, compress it. if ((writer.Header & 0x40) != 0) return Compress(writer.Data); else return writer.Data; } /// /// IExtension implementation to possible decompress the data. /// public BufferSegment Decode(byte header, BufferSegment data) { // Is the server compressed the data? If so, decompress it. if ((header & 0x40) != 0) return Decompress(data); else return data; } #endregion #region Private Helper Functions /// /// A function to compress and return the data parameter with possible context takeover support (reusing the DeflateStream). /// private BufferSegment Compress(BufferSegment data) { CheckDisposed(); if (compressorOutputStream == null) compressorOutputStream = new BufferPoolMemoryStream(); compressorOutputStream.SetLength(0); if (compressorDeflateStream == null) { compressorDeflateStream = new DeflateStream(compressorOutputStream, CompressionMode.Compress, this.Level, false, this.ClientMaxWindowBits); compressorDeflateStream.FlushMode = FlushType.Sync; } BufferSegment result = BufferSegment.Empty; try { compressorDeflateStream.Write(data.Data, data.Offset, data.Count); compressorDeflateStream.Flush(); compressorOutputStream.Position = 0; // http://tools.ietf.org/html/rfc7692#section-7.2.1 // 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. compressorOutputStream.SetLength(compressorOutputStream.Length - 4); result = compressorOutputStream.ToBufferSegment(); } finally { if (this.ClientNoContextTakeover) { compressorDeflateStream.Dispose(); compressorDeflateStream = null; compressorOutputStream = null; } } return result; } /// /// A function to decompress and return the data parameter with possible context takeover support (reusing the DeflateStream). /// private BufferSegment Decompress(BufferSegment data) { CheckDisposed(); if (decompressorInputStream == null) decompressorInputStream = new BufferPoolMemoryStream(data.Count + 4); decompressorInputStream.Write(data.Data, data.Offset, data.Count); // http://tools.ietf.org/html/rfc7692#section-7.2.2 // Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the payload of the message. decompressorInputStream.Write(PerMessageCompression.Trailer, 0, PerMessageCompression.Trailer.Length); decompressorInputStream.Position = 0; if (decompressorDeflateStream == null) { decompressorDeflateStream = new DeflateStream(decompressorInputStream, CompressionMode.Decompress, CompressionLevel.Default, false, this.ServerMaxWindowBits); decompressorDeflateStream.FlushMode = FlushType.Sync; } if (decompressorOutputStream == null) decompressorOutputStream = new BufferPoolMemoryStream(); decompressorOutputStream.SetLength(0); byte[] copyBuffer = BufferPool.Get(1024, true); int readCount; while ((readCount = decompressorDeflateStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0) decompressorOutputStream.Write(copyBuffer, 0, readCount); BufferPool.Release(copyBuffer); decompressorDeflateStream.SetLength(0); var result = decompressorOutputStream.ToBufferSegment(); if (this.ServerNoContextTakeover) { decompressorDeflateStream.Dispose(); decompressorDeflateStream = null; decompressorInputStream = null; } return result; } #endregion bool _disposed; void CheckDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(PerMessageCompression)); } void IDisposable.Dispose() { _disposed = true; compressorDeflateStream?.Dispose(); compressorOutputStream?.Dispose(); decompressorDeflateStream?.Dispose(); decompressorInputStream?.Dispose(); decompressorOutputStream?.Dispose(); } } } #endif