#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2 && !BESTHTTP_DISABLE_WEBSOCKET using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text; using BestHTTP.Connections.HTTP2; using BestHTTP.Extensions; using BestHTTP.Logger; using BestHTTP.PlatformSupport.Memory; using BestHTTP.WebSocket.Frames; namespace BestHTTP.WebSocket { /// /// Implements RFC 8441 (https://tools.ietf.org/html/rfc8441) to use Websocket over HTTP/2 /// public sealed class OverHTTP2 : WebSocketBaseImplementation, IHeartbeat { public override int BufferedAmount { get => this._bufferedAmount; } internal volatile int _bufferedAmount; public override bool IsOpen => this.State == WebSocketStates.Open; public override int Latency { get { return this.Parent.StartPingThread ? base.Latency : (int)this.http2Handler.Latency; } } private List IncompleteFrames = new List(); private HTTP2Handler http2Handler; /// /// True if we sent out a Close message to the server /// internal volatile bool closeSent; /// /// When we sent out the last ping. /// private DateTime lastPing = DateTime.MinValue; private bool waitingForPong = false; /// /// A circular buffer to store the last N rtt times calculated by the pong messages. /// private CircularBuffer rtts = new CircularBuffer(WebSocketResponse.RTTBufferCapacity); private PeekableIncomingSegmentStream incomingSegmentStream = new PeekableIncomingSegmentStream(); private ConcurrentQueue CompletedFrames = new ConcurrentQueue(); internal ConcurrentQueue frames = new ConcurrentQueue(); public OverHTTP2(WebSocket parent, Uri uri, string origin, string protocol) : base(parent, uri, origin, protocol) { // use https scheme so it will be served over HTTP/2. Thre request's Tag will be set to this class' instance so HTTP2Handler will know it has to create a HTTP2WebSocketStream instance to // process the request. string scheme = "https"; int port = uri.Port != -1 ? uri.Port : 443; base.Uri = new Uri(scheme + "://" + uri.Host + ":" + port + uri.GetRequestPathAndQueryURL()); } internal void SetHTTP2Handler(HTTP2Handler handler) => this.http2Handler = handler; protected override void CreateInternalRequest() { HTTPManager.Logger.Verbose("OverHTTP2", "CreateInternalRequest", this.Parent.Context); base._internalRequest = new HTTPRequest(base.Uri, HTTPMethods.Connect, OnInternalRequestCallback); base._internalRequest.Context.Add("WebSocket", this.Parent.Context); base._internalRequest.SetHeader(":protocol", "websocket"); // The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a // randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection. base._internalRequest.SetHeader("sec-webSocket-key", WebSocket.GetSecKey(new object[] { this, InternalRequest, base.Uri, new object() })); // The request MUST include a header field with the name |Origin| [RFC6454] if the request is coming from a browser client. // If the connection is from a non-browser client, the request MAY include this header field if the semantics of that client match the use-case described here for browser clients. // More on Origin Considerations: http://tools.ietf.org/html/rfc6455#section-10.2 if (!string.IsNullOrEmpty(base.Origin)) base._internalRequest.SetHeader("origin", base.Origin); // The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13. base._internalRequest.SetHeader("sec-webSocket-version", "13"); if (!string.IsNullOrEmpty(base.Protocol)) base._internalRequest.SetHeader("sec-webSocket-protocol", base.Protocol); // Disable caching base._internalRequest.SetHeader("cache-control", "no-cache"); base._internalRequest.SetHeader("pragma", "no-cache"); #if !BESTHTTP_DISABLE_CACHING base._internalRequest.DisableCache = true; #endif base._internalRequest.OnHeadersReceived += OnHeadersReceived; // set a fake upload stream, so HPACKEncoder will not set the END_STREAM flag base._internalRequest.UploadStream = new MemoryStream(0); base._internalRequest.UseUploadStreamLength = false; this.LastMessageReceived = DateTime.Now; base._internalRequest.Tag = this; if (this.Parent.OnInternalRequestCreated != null) { try { this.Parent.OnInternalRequestCreated(this.Parent, base._internalRequest); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "CreateInternalRequest", ex, this.Parent.Context); } } } private void OnHeadersReceived(HTTPRequest req, HTTPResponse resp, Dictionary> newHeaders) { HTTPManager.Logger.Verbose("OverHTTP2", $"OnHeadersReceived - StatusCode: {resp?.StatusCode}", this.Parent.Context); if (resp != null && resp.StatusCode == 200) { if (this.Parent.Extensions != null) { for (int i = 0; i < this.Parent.Extensions.Length; ++i) { var ext = this.Parent.Extensions[i]; try { if (ext != null && !ext.ParseNegotiation(resp)) this.Parent.Extensions[i] = null; // Keep extensions only that successfully negotiated } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "ParseNegotiation", ex, this.Parent.Context); // Do not try to use a defective extension in the future this.Parent.Extensions[i] = null; } } } this.State = WebSocketStates.Open; if (this.Parent.OnOpen != null) { try { this.Parent.OnOpen(this.Parent); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "OnOpen", ex, this.Parent.Context); } } if (this.Parent.StartPingThread) { this.LastMessageReceived = DateTime.Now; SendPing(); } } else req.Abort(); } private static bool CanReadFullFrame(PeekableIncomingSegmentStream stream) { if (stream.Length < 2) return false; stream.BeginPeek(); if (stream.PeekByte() == -1) return false; int maskAndLength = stream.PeekByte(); if (maskAndLength == -1) return false; // The second byte is the Mask Bit and the length of the payload data var HasMask = (maskAndLength & 0x80) != 0; // if 0-125, that is the payload length. var Length = (UInt64)(maskAndLength & 127); // If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length. if (Length == 126) { byte[] rawLen = BufferPool.Get(2, true); for (int i = 0; i < 2; i++) { int data = stream.PeekByte(); if (data < 0) return false; rawLen[i] = (byte)data; } if (BitConverter.IsLittleEndian) Array.Reverse(rawLen, 0, 2); Length = (UInt64)BitConverter.ToUInt16(rawLen, 0); BufferPool.Release(rawLen); } else if (Length == 127) { // If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the // most significant bit MUST be 0) are the payload length. byte[] rawLen = BufferPool.Get(8, true); for (int i = 0; i < 8; i++) { int data = stream.PeekByte(); if (data < 0) return false; rawLen[i] = (byte)data; } if (BitConverter.IsLittleEndian) Array.Reverse(rawLen, 0, 8); Length = (UInt64)BitConverter.ToUInt64(rawLen, 0); BufferPool.Release(rawLen); } // Header + Mask&Length Length += 2; // 4 bytes for Mask if present if (HasMask) Length += 4; return stream.Length >= (long)Length; } internal void OnReadThread(BufferSegment buffer) { this.LastMessageReceived = DateTime.Now; this.incomingSegmentStream.Write(buffer); while (CanReadFullFrame(this.incomingSegmentStream)) { WebSocketFrameReader frame = new WebSocketFrameReader(); frame.Read(this.incomingSegmentStream); if (HTTPManager.Logger.Level == Logger.Loglevels.All) HTTPManager.Logger.Verbose("OverHTTP2", "Frame received: " + frame.ToString(), this.Parent.Context); if (!frame.IsFinal) { if (this.Parent.OnIncompleteFrame == null) IncompleteFrames.Add(frame); else CompletedFrames.Enqueue(frame); continue; } switch (frame.Type) { // For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4 // A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1. case WebSocketFrameTypes.Continuation: // Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase. if (this.Parent.OnIncompleteFrame == null) { frame.Assemble(IncompleteFrames); // Remove all incomplete frames IncompleteFrames.Clear(); // Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual. goto case WebSocketFrameTypes.Binary; } else { CompletedFrames.Enqueue(frame); } break; case WebSocketFrameTypes.Text: case WebSocketFrameTypes.Binary: frame.DecodeWithExtensions(this.Parent); CompletedFrames.Enqueue(frame); break; // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame. case WebSocketFrameTypes.Ping: if (!closeSent && this.State != WebSocketStates.Closed) { // copy data set to true here, as the frame's data is released back to the pool after the switch Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Pong, frame.Data, true, true, true)); } break; case WebSocketFrameTypes.Pong: // https://tools.ietf.org/html/rfc6455#section-5.5 // A Pong frame MAY be sent unsolicited. This serves as a // unidirectional heartbeat. A response to an unsolicited Pong frame is // not expected. if (!waitingForPong) break; waitingForPong = false; // the difference between the current time and the time when the ping message is sent TimeSpan diff = DateTime.Now - lastPing; // add it to the buffer this.rtts.Add((int)diff.TotalMilliseconds); // and calculate the new latency base.Latency = CalculateLatency(); break; // If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response. case WebSocketFrameTypes.ConnectionClose: HTTPManager.Logger.Information("OverHTTP2", "ConnectionClose packet received!", this.Parent.Context); CompletedFrames.Enqueue(frame); if (!closeSent) Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, BufferSegment.Empty)); this.State = WebSocketStates.Closed; break; } } } private void OnInternalRequestCallback(HTTPRequest req, HTTPResponse resp) { HTTPManager.Logger.Verbose("OverHTTP2", $"OnInternalRequestCallback - this.State: {this.State}", this.Parent.Context); // If it's already closed, all events are called too. if (this.State == WebSocketStates.Closed) return; if (this.State == WebSocketStates.Connecting && HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback) { this.Parent.FallbackToHTTP1(); return; } string reason = string.Empty; switch (req.State) { case HTTPRequestStates.Finished: HTTPManager.Logger.Information("OverHTTP2", string.Format("Request finished. Status Code: {0} Message: {1}", resp.StatusCode.ToString(), resp.Message), this.Parent.Context); if (resp.StatusCode == 101) { // The request upgraded successfully. return; } else reason = string.Format("Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}", resp.StatusCode, resp.Message, resp.DataAsText); break; // The request finished with an unexpected error. The request's Exception property may contain more info about the error. case HTTPRequestStates.Error: reason = "Request Finished with Error! " + (req.Exception != null ? ("Exception: " + req.Exception.Message + req.Exception.StackTrace) : string.Empty); break; // The request aborted, initiated by the user. case HTTPRequestStates.Aborted: reason = "Request Aborted!"; break; // Connecting to the server is timed out. case HTTPRequestStates.ConnectionTimedOut: reason = "Connection Timed Out!"; break; // The request didn't finished in the given time. case HTTPRequestStates.TimedOut: reason = "Processing the request Timed Out!"; break; default: return; } if (this.State != WebSocketStates.Connecting || !string.IsNullOrEmpty(reason)) { if (this.Parent.OnError != null) { try { this.Parent.OnError(this.Parent, reason); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context); } } else if (!HTTPManager.IsQuitting) HTTPManager.Logger.Error("OverHTTP2", reason, this.Parent.Context); } else if (this.Parent.OnClosed != null) { try { this.Parent.OnClosed(this.Parent, (ushort)WebSocketStausCodes.NormalClosure, "Closed while opening"); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "OnClosed", ex, this.Parent.Context); } } this.State = WebSocketStates.Closed; } public override void StartOpen() { HTTPManager.Logger.Verbose("OverHTTP2", "StartOpen", this.Parent.Context); if (this.Parent.Extensions != null) { try { for (int i = 0; i < this.Parent.Extensions.Length; ++i) { var ext = this.Parent.Extensions[i]; if (ext != null) ext.AddNegotiation(base.InternalRequest); } } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "Open", ex, this.Parent.Context); } } base.InternalRequest.Send(); HTTPManager.Heartbeats.Subscribe(this); this.State = WebSocketStates.Connecting; } public override void StartClose(ushort code, string message) { HTTPManager.Logger.Verbose("OverHTTP2", "StartClose", this.Parent.Context); if (this.State == WebSocketStates.Connecting) { if (this.InternalRequest != null) this.InternalRequest.Abort(); this.State = WebSocketStates.Closed; if (this.Parent.OnClosed != null) this.Parent.OnClosed(this.Parent, code, message); } else { Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, message), true, false, false)); this.State = WebSocketStates.Closing; } } public override void Send(string message) { if (message == null) throw new ArgumentNullException("message must not be null!"); int count = System.Text.Encoding.UTF8.GetByteCount(message); byte[] data = BufferPool.Get(count, true); System.Text.Encoding.UTF8.GetBytes(message, 0, message.Length, data, 0); SendAsText(data.AsBuffer(0, count)); } public override void Send(byte[] buffer) { if (buffer == null) throw new ArgumentNullException("data must not be null!"); Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(buffer, 0, buffer.Length))); } public override void Send(byte[] data, ulong offset, ulong count) { if (data == null) throw new ArgumentNullException("data must not be null!"); if (offset + count > (ulong)data.Length) throw new ArgumentOutOfRangeException("offset + count >= data.Length"); Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Binary, new BufferSegment(data, (int)offset, (int)count), true, true)); } public override void Send(WebSocketFrame frame) { if (this.State == WebSocketStates.Closed || closeSent) return; this.frames.Enqueue(frame); this.http2Handler.SignalRunnerThread(); this._bufferedAmount += frame.Data.Count; if (frame.Type == WebSocketFrameTypes.ConnectionClose) this.closeSent = true; } public override void SendAsBinary(BufferSegment data) { Send(WebSocketFrameTypes.Binary, data); } public override void SendAsText(BufferSegment data) { Send(WebSocketFrameTypes.Text, data); } private void Send(WebSocketFrameTypes type, BufferSegment data) { Send(new WebSocketFrame(this.Parent, type, data, true, true, false)); } private int CalculateLatency() { if (this.rtts.Count == 0) return 0; int sumLatency = 0; for (int i = 0; i < this.rtts.Count; ++i) sumLatency += this.rtts[i]; return sumLatency / this.rtts.Count; } internal void PreReadCallback() { if (this.Parent.StartPingThread) { DateTime now = DateTime.Now; if (!waitingForPong && now - LastMessageReceived >= TimeSpan.FromMilliseconds(this.Parent.PingFrequency)) SendPing(); if (waitingForPong && now - lastPing > this.Parent.CloseAfterNoMessage) { if (this.State != WebSocketStates.Closed) { HTTPManager.Logger.Warning("OverHTTP2", string.Format("No message received in the given time! Closing WebSocket. LastPing: {0}, PingFrequency: {1}, Close After: {2}, Now: {3}", this.lastPing, TimeSpan.FromMilliseconds(this.Parent.PingFrequency), this.Parent.CloseAfterNoMessage, now), this.Parent.Context); CloseWithError("No message received in the given time!"); } } } } public void OnHeartbeatUpdate(TimeSpan dif) { DateTime now = DateTime.Now; switch (this.State) { case WebSocketStates.Connecting: if (now - this.InternalRequest.Timing.Start >= this.Parent.CloseAfterNoMessage) { if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableImplementationFallback) { this.State = WebSocketStates.Closed; this.InternalRequest.OnHeadersReceived = null; this.InternalRequest.Callback = null; this.Parent.FallbackToHTTP1(); } else { CloseWithError("WebSocket Over HTTP/2 Implementation failed to connect in the given time!"); } } break; default: while (CompletedFrames.TryDequeue(out var frame)) { // Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here try { switch (frame.Type) { case WebSocketFrameTypes.Continuation: if (HTTPManager.Logger.Level == Loglevels.All) HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - OnIncompleteFrame", this.Parent.Context); if (this.Parent.OnIncompleteFrame != null) this.Parent.OnIncompleteFrame(this.Parent, frame); break; case WebSocketFrameTypes.Text: // Any not Final frame is handled as a fragment if (!frame.IsFinal) goto case WebSocketFrameTypes.Continuation; if (HTTPManager.Logger.Level == Loglevels.All) HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnText(\"{frame.DataAsText}\")", this.Parent.Context); if (this.Parent.OnMessage != null) this.Parent.OnMessage(this.Parent, frame.DataAsText); break; case WebSocketFrameTypes.Binary: // Any not Final frame is handled as a fragment if (!frame.IsFinal) goto case WebSocketFrameTypes.Continuation; if (HTTPManager.Logger.Level == Loglevels.All) HTTPManager.Logger.Verbose("OverHTTP2", $"HandleEvents - OnBinary({frame.Data})", this.Parent.Context); if (this.Parent.OnBinary != null) { var data = new byte[frame.Data.Count]; Array.Copy(frame.Data.Data, frame.Data.Offset, data, 0, frame.Data.Count); this.Parent.OnBinary(this.Parent, data); } if (this.Parent.OnBinaryNoAlloc != null) this.Parent.OnBinaryNoAlloc(this.Parent, frame.Data); break; case WebSocketFrameTypes.ConnectionClose: HTTPManager.Logger.Verbose("OverHTTP2", "HandleEvents - Calling OnClosed", this.Parent.Context); if (this.Parent.OnClosed != null) { try { UInt16 statusCode = 0; string msg = string.Empty; // If we received any data, we will get the status code and the message from it if (/*CloseFrame != null && */ frame.Data != BufferSegment.Empty && frame.Data.Count >= 2) { if (BitConverter.IsLittleEndian) Array.Reverse(frame.Data.Data, frame.Data.Offset, 2); statusCode = BitConverter.ToUInt16(frame.Data.Data, frame.Data.Offset); if (frame.Data.Count > 2) msg = Encoding.UTF8.GetString(frame.Data.Data, frame.Data.Offset + 2, frame.Data.Count - 2); frame.ReleaseData(); } this.Parent.OnClosed(this.Parent, statusCode, msg); this.Parent.OnClosed = null; } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "HandleEvents - OnClosed", ex, this.Parent.Context); } } HTTPManager.Heartbeats.Unsubscribe(this); break; } } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", string.Format("HandleEvents({0})", frame.ToString()), ex, this.Parent.Context); } finally { frame.ReleaseData(); } } break; } } /// /// Next interaction relative to *now*. /// public TimeSpan GetNextInteraction() { if (waitingForPong) return TimeSpan.MaxValue; return (LastMessageReceived + TimeSpan.FromMilliseconds(this.Parent.PingFrequency)) - DateTime.Now; } private void SendPing() { HTTPManager.Logger.Information("OverHTTP2", "Sending Ping frame, waiting for a pong...", this.Parent.Context); lastPing = DateTime.Now; waitingForPong = true; Send(new WebSocketFrame(this.Parent, WebSocketFrameTypes.Ping, BufferSegment.Empty)); } private void CloseWithError(string message) { HTTPManager.Logger.Verbose("OverHTTP2", $"CloseWithError(\"{message}\")", this.Parent.Context); this.State = WebSocketStates.Closed; if (this.Parent.OnError != null) { try { this.Parent.OnError(this.Parent, message); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP2", "OnError", ex, this.Parent.Context); } } this.InternalRequest.Abort(); } } } #endif