using System; using System.Text; using Best.HTTP.Shared.Extensions; using Best.HTTP.Shared.Logger; using Best.HTTP.Shared.PlatformSupport.Memory; using Best.HTTP.Shared.Streams; using Best.HTTP.HostSetting; using Best.HTTP.Request.Settings; using Best.HTTP.Shared; using Best.HTTP.Hosts.Connections; using Best.WebSockets.Implementations; #if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL using Best.HTTP.Hosts.Connections.HTTP2; #endif #if !UNITY_WEBGL || UNITY_EDITOR using Best.WebSockets.Extensions; #endif using Best.HTTP.Shared.Compression.Zlib; /* * This is a wrapper class organizing where and how the websocket request is sent out. If there's an already open HTTP/2 connection with * an ENABLE_CONNECT_PROTOCOL flag, it tries using the HTTP/2 connection first and if it fails, a new HTTP/1 connection is created. * */ namespace Best.WebSockets { /// /// Implements the WebSocket standard for duplex, two-way communications. /// public sealed class WebSocket { /// /// Maximum payload size of a websocket frame. Its default value is 32 KiB. /// public static uint MaxFragmentSize = UInt16.MaxValue / 2; #if !UNITY_WEBGL || UNITY_EDITOR public static IExtension[] GetDefaultExtensions() { return new IExtension[] { new PerMessageCompression(/*compression level: */ CompressionLevel.Default, /*clientNoContextTakeover: */ false, /*serverNoContextTakeover: */ false, /*clientMaxWindowBits: */ ZlibConstants.WindowBitsMax, /*desiredServerMaxWindowBits: */ ZlibConstants.WindowBitsMax, /*minDatalengthToCompress: */ PerMessageCompression.MinDataLengthToCompressDefault) }; } #endif public WebSocketStates State { get { return this.implementation.State; } } /// /// The connection to the WebSocket server is open. /// public bool IsOpen { get { return this.implementation.IsOpen; } } /// /// Data waiting to be written to the wire. /// public int BufferedAmount { get { return this.implementation.BufferedAmount; } } #if !UNITY_WEBGL || UNITY_EDITOR /// /// Set to true to start sending Ping frames to the WebSocket server. /// public bool SendPings { get; set; } /// /// The delay between two Pings in milliseconds. Minimum value is 100ms, default is 10 seconds. /// public TimeSpan PingFrequency { get; set; } /// /// If set to true, the plugin will close the connection and emit an event if no /// message is received from the server in the given time. Its default value is 2 sec. /// public TimeSpan CloseAfterNoMessage { get; set; } /// /// The internal object. /// public HTTP.HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } } /// /// implementations the plugin will negotiate with the server to use. /// public IExtension[] Extensions { get; private set; } /// /// Latency calculated from ping-pong message round-trip times. /// public int Latency { get { return this.implementation.Latency; } } /// /// When the WebSocket instance received the last message from the server. /// public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } } /// /// When the Websocket Over HTTP/2 implementation fails to connect and . is true, the plugin tries to fall back to the HTTP/1 implementation. /// When this happens a new is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied. /// public Action OnInternalRequestCreated; #endif /// /// Called when the connection to the WebSocket server is established. /// public OnWebSocketOpenDelegate OnOpen; /// /// Called when a new textual message is received from the server. /// public OnWebSocketMessageDelegate OnMessage; /// /// Called when a Binary message received. /// The content of the must be used or copied to a new array in the callbacks because the plugin reuses the memory immediately after the callback by placing it back to the ! /// /// Note that the memory will be reused when this event returns. Either process it in this call or make a copy from the received data. public OnWebSocketBinaryNoAllocDelegate OnBinary; /// /// Called when the WebSocket connection is closed. /// public OnWebSocketClosedDelegate OnClosed; /// /// Logging context of this websocket instance. /// public LoggingContext Context { get; private set; } /// /// The underlying, real implementation. /// private WebSocketBaseImplementation implementation; /// /// Creates a WebSocket instance from the given uri. /// /// The uri of the WebSocket server public WebSocket(Uri uri) :this(uri, string.Empty, string.Empty) { #if (!UNITY_WEBGL || UNITY_EDITOR) this.Extensions = WebSocket.GetDefaultExtensions(); #endif } #if !UNITY_WEBGL || UNITY_EDITOR /// /// Creates a WebSocket instance from the given uri. /// /// Uri of the WebSocket endpoint. /// Where the WebSocket originating from. /// The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used. public WebSocket(Uri uri, string origin, string protocol) :this(uri, origin, protocol, null) { #if (!UNITY_WEBGL || UNITY_EDITOR) this.Extensions = WebSocket.GetDefaultExtensions(); #endif } #endif /// /// Creates a WebSocket instance from the given uri, protocol and origin. /// /// The uri of the WebSocket server /// Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect. /// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code. /// The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used. /// Optional implementations public WebSocket(Uri uri, string origin, string protocol #if !UNITY_WEBGL || UNITY_EDITOR , params IExtension[] extensions #endif ) { this.Context = new LoggingContext(this); #if !UNITY_WEBGL || UNITY_EDITOR this.Extensions = extensions; #endif SelectImplementation(uri, origin, protocol); // Under WebGL when only the WebSocket protocol is used Setup() isn't called, so we have to call it here. HTTPManager.Setup(); } internal WebSocketBaseImplementation SelectImplementation(Uri uri, string origin, string protocol) { #if !UNITY_WEBGL || UNITY_EDITOR #if !BESTHTTP_DISABLE_ALTERNATE_SSL if (HTTPProtocolFactory.IsSecureProtocol(uri) && HTTPManager.PerHostSettings.Get(uri).HTTP2ConnectionSettings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2) { // Try to find a HTTP/2 connection that supports the connect protocol. var connectionKey = HostKey.From(new UriBuilder("https", uri.Host, uri.Port).Uri, GetProxy(uri)); var con = HostManager.GetHostVariant(connectionKey).Find(c => { var httpConnection = c as HTTPOverTCPConnection; var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer; return http2Handler != null && http2Handler.settings.RemoteSettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0; }); if (con != null) { HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context); var httpConnection = con as HTTPOverTCPConnection; var http2Handler = httpConnection?.requestHandler as HTTP2ContentConsumer; this.implementation = new OverHTTP2(this, uri, origin, protocol); } } #endif if (this.implementation == null) this.implementation = new OverHTTP1(this, uri, origin, protocol); #else this.implementation = new WebGLBrowser(this, uri, origin, protocol); #endif return this.implementation; } #if !UNITY_WEBGL || UNITY_EDITOR internal void FallbackToHTTP1() { HTTPManager.Logger.Verbose("WebSocket", "FallbackToHTTP1", this.Context); if (this.implementation == null) return; this.implementation = new OverHTTP1(this, this.implementation.Uri, this.implementation.Origin, this.implementation.Protocol); this.implementation.StartOpen(); } #endif /// /// Start the opening process. /// /// It's a non-blocking call. To get notified when the WebSocket instance is considered open and can send/receive, use the event. public void Open() { this.implementation.StartOpen(); } /// /// It will send the given textual message to the remote server. /// public void Send(string message) { if (!IsOpen) return; this.implementation.Send(message); } /// /// It will send the given binary message to the remote server. /// public void Send(byte[] buffer) { if (!IsOpen) return; this.implementation.Send(buffer); } /// /// It will send the given binary message to the remote server. /// public void Send(byte[] buffer, ulong offset, ulong count) { if (!IsOpen) return; this.implementation.Send(buffer, offset, count); } /// /// Will send the data in one or more binary frame and takes ownership over it calling BufferPool.Release when the data sent. /// public void SendAsBinary(BufferSegment data) { if (!IsOpen) { BufferPool.Release(data); return; } this.implementation.SendAsBinary(data); } /// /// Will send data as a text frame and takes owenership over the memory region releasing it to the BufferPool as soon as possible. /// public void SendAsText(BufferSegment data) { if (!IsOpen) { BufferPool.Release(data); return; } this.implementation.SendAsText(data); } /// /// It will initiate the closing of the connection to the server. /// public void Close() { if (State >= WebSocketStates.Closing) return; this.implementation.StartClose(WebSocketStatusCodes.NormalClosure, "Bye!"); } /// /// It will initiate the closing of the connection to the server sending the given code and message. /// public void Close(WebSocketStatusCodes code, string message) { if (!IsOpen) return; this.implementation.StartClose(code, message); } #if !UNITY_WEBGL || UNITY_EDITOR internal ProxySettings GetProxy(Uri uri) { // WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy var proxy = HTTPManager.Proxy as HTTP.Proxies.HTTPProxy; if (proxy != null && proxy.UseProxyForAddress(uri)) proxy = new HTTP.Proxies.HTTPProxy(proxy.Address, proxy.Credentials, false, /*turn on 'tunneling'*/ false, /*sendWholeUri*/ proxy.NonTransparentForHTTPS); return new ProxySettings { Proxy = proxy }; } internal void DisposeExtensions() { if (this.Extensions != null) { for (int i = 0; i < this.Extensions.Length; ++i) { var ext = this.Extensions[i]; try { ext?.Dispose(); } catch (Exception ex) { HTTPManager.Logger.Exception("OverHTTP1", "OnInternalRequestCallback - extension dispose", ex, this.Context); } this.Extensions[i] = null; } this.Extensions = null; } } #endif #if !UNITY_WEBGL || UNITY_EDITOR internal static BufferSegment EncodeCloseData(WebSocketStatusCodes code, string message) { //If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer // (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer, // the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification. // This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection. int msgLen = Encoding.UTF8.GetByteCount(message); using (var ms = new BufferPoolMemoryStream(2 + msgLen)) { byte[] buff = BitConverter.GetBytes((ushort)code); if (BitConverter.IsLittleEndian) Array.Reverse(buff, 0, buff.Length); ms.Write(buff, 0, buff.Length); buff = Encoding.UTF8.GetBytes(message); ms.Write(buff, 0, buff.Length); buff = ms.ToArray(); return buff.AsBuffer(buff.Length); } } internal static string GetSecKey(object[] from) { const int keysLength = 16; byte[] keys = BufferPool.Get(keysLength, true); int pos = 0; for (int i = 0; i < from.Length; ++i) { byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode()); for (int cv = 0; cv < hash.Length && pos < keysLength; ++cv) keys[pos++] = hash[cv]; } var result = Convert.ToBase64String(keys, 0, keysLength); BufferPool.Release(keys); return result; } #endif } }