using System; using System.Collections.Generic; using UnityEngine; #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) using BestHTTP.Caching; #endif using BestHTTP.Extensions; using BestHTTP.Logger; using BestHTTP.Statistics; namespace BestHTTP { /// /// /// public static class HTTPManager { // Static constructor. Setup default values static HTTPManager() { MaxConnectionPerServer = 4; KeepAliveDefaultValue = true; MaxPathLength = 255; MaxConnectionIdleTime = TimeSpan.FromSeconds(20); #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR) IsCookiesEnabled = true; #endif CookieJarSize = 10 * 1024 * 1024; EnablePrivateBrowsing = false; ConnectTimeout = TimeSpan.FromSeconds(20); RequestTimeout = TimeSpan.FromSeconds(60); // Set the default logger mechanism logger = new BestHTTP.Logger.DefaultLogger(); #if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR) DefaultCertificateVerifyer = null; UseAlternateSSLDefaultValue = true; #endif } #region Global Options /// /// The maximum active TCP connections that the client will maintain to a server. Default value is 4. Minimum value is 1. /// public static byte MaxConnectionPerServer { get{ return maxConnectionPerServer; } set { if (value <= 0) throw new ArgumentOutOfRangeException("MaxConnectionPerServer must be greater than 0!"); maxConnectionPerServer = value; } } private static byte maxConnectionPerServer; /// /// Default value of a HTTP request's IsKeepAlive value. Default value is true. If you make rare request to the server it should be changed to false. /// public static bool KeepAliveDefaultValue { get; set; } #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) /// /// Set to true, if caching is prohibited. /// public static bool IsCachingDisabled { get; set; } #endif /// /// How many time must be passed to destroy that connection after a connection finished its last request. Its default value is 20 seconds. /// public static TimeSpan MaxConnectionIdleTime { get; set; } #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR) /// /// Set to false to disable all Cookie. It's default value is true. /// public static bool IsCookiesEnabled { get; set; } #endif /// /// Size of the Cookie Jar in bytes. It's default value is 10485760 (10 MB). /// public static uint CookieJarSize { get; set; } /// /// If this property is set to true, then new cookies treated as session cookies and these cookies are not saved to disk. Its default value is false; /// public static bool EnablePrivateBrowsing { get; set; } /// /// Global, default value of the HTTPRequest's ConnectTimeout property. If set to TimeSpan.Zero or lower, no connect timeout logic is executed. Default value is 20 seconds. /// public static TimeSpan ConnectTimeout { get; set; } /// /// Global, default value of the HTTPRequest's Timeout property. Default value is 60 seconds. /// public static TimeSpan RequestTimeout { get; set; } #if !((BESTHTTP_DISABLE_CACHING && BESTHTTP_DISABLE_COOKIES) || (UNITY_WEBGL && !UNITY_EDITOR)) /// /// By default the plugin will save all cache and cookie data under the path returned by Application.persistentDataPath. /// You can assign a function to this delegate to return a custom root path to define a new path. /// This delegate will be called on a non Unity thread! /// public static System.Func RootCacheFolderProvider { get; set; } #endif #if !BESTHTTP_DISABLE_PROXY /// /// The global, default proxy for all HTTPRequests. The HTTPRequest's Proxy still can be changed per-request. Default value is null. /// public static HTTPProxy Proxy { get; set; } #endif /// /// Heartbeat manager to use less threads in the plugin. The heartbeat updates are called from the OnUpdate function. /// public static HeartbeatManager Heartbeats { get { if (heartbeats == null) heartbeats = new HeartbeatManager(); return heartbeats; } } private static HeartbeatManager heartbeats; /// /// A basic BestHTTP.Logger.ILogger implementation to be able to log intelligently additional informations about the plugin's internal mechanism. /// public static BestHTTP.Logger.ILogger Logger { get { // Make sure that it has a valid logger instance. if (logger == null) { logger = new DefaultLogger(); logger.Level = Loglevels.None; } return logger; } set { logger = value; } } private static BestHTTP.Logger.ILogger logger; #if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR) /// /// The default ICertificateVerifyer implementation that the plugin will use when the request's UseAlternateSSL property is set to true. /// public static Org.BouncyCastle.Crypto.Tls.ICertificateVerifyer DefaultCertificateVerifyer { get; set; } /// /// The default IClientCredentialsProvider implementation that the plugin will use when the request's UseAlternateSSL property is set to true. /// public static Org.BouncyCastle.Crypto.Tls.IClientCredentialsProvider DefaultClientCredentialsProvider { get; set; } /// /// The default value for the HTTPRequest's UseAlternateSSL property. /// public static bool UseAlternateSSLDefaultValue { get; set; } #endif #if !NETFX_CORE && !UNITY_WP8 public static Func DefaultCertificationValidator { get; set; } #endif /// /// Setting this option to true, the processing connection will set the TCP NoDelay option to send out data as soon as it can. /// public static bool TryToMinimizeTCPLatency = false; public static int SendBufferSize = 65 * 1024; public static int ReceiveBufferSize = 65 * 1024; /// /// On most systems the maximum length of a path is around 255 character. If a cache entity's path is longer than this value it doesn't get cached. There no platform independent API to query the exact value on the current system, but it's /// exposed here and can be overridden. It's default value is 255. /// internal static int MaxPathLength { get; set; } #endregion #region Manager variables /// /// All connection has a reference in this Dictionary until it's removed completely. /// private static Dictionary> Connections = new Dictionary>(); /// /// Active connections. These connections all has a request to process. /// private static List ActiveConnections = new List(); /// /// Free connections. They can be removed completely after a specified time. /// private static List FreeConnections = new List(); /// /// Connections that recycled in the Update loop. If they are not used in the same loop to process a request, they will be transferred to the FreeConnections list. /// private static List RecycledConnections = new List(); /// /// List of request that have to wait until there is a free connection to the server. /// private static List RequestQueue = new List(); private static bool IsCallingCallbacks; internal static System.Object Locker = new System.Object(); internal static bool IsQuitting { get; private set; } #endregion #region Public Interface public static void Setup() { HTTPUpdateDelegator.CheckInstance(); #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) HTTPCacheService.CheckSetup(); #endif #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR) Cookies.CookieJar.SetupFolder(); #endif } public static HTTPRequest SendRequest(string url, OnRequestFinishedDelegate callback) { return SendRequest(new HTTPRequest(new Uri(url), HTTPMethods.Get, callback)); } public static HTTPRequest SendRequest(string url, HTTPMethods methodType, OnRequestFinishedDelegate callback) { return SendRequest(new HTTPRequest(new Uri(url), methodType, callback)); } public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, OnRequestFinishedDelegate callback) { return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, callback)); } public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, bool disableCache, OnRequestFinishedDelegate callback) { return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, disableCache, callback)); } public static HTTPRequest SendRequest(HTTPRequest request) { lock (Locker) { Setup(); if (IsCallingCallbacks) { request.State = HTTPRequestStates.Queued; RequestQueue.Add(request); } else SendRequestImpl(request); return request; } } public static GeneralStatistics GetGeneralStatistics(StatisticsQueryFlags queryFlags) { GeneralStatistics stat = new GeneralStatistics(); stat.QueryFlags = queryFlags; if ((queryFlags & StatisticsQueryFlags.Connections) != 0) { int connections = 0; foreach(var conn in HTTPManager.Connections) { if (conn.Value != null) connections += conn.Value.Count; } #if !BESTHTTP_DISABLE_WEBSOCKET && UNITY_WEBGL && !UNITY_EDITOR connections += WebSocket.WebSocket.WebSockets.Count; #endif stat.Connections = connections; stat.ActiveConnections = ActiveConnections.Count #if !BESTHTTP_DISABLE_WEBSOCKET && UNITY_WEBGL && !UNITY_EDITOR + WebSocket.WebSocket.WebSockets.Count #endif ; stat.FreeConnections = FreeConnections.Count; stat.RecycledConnections = RecycledConnections.Count; stat.RequestsInQueue = RequestQueue.Count; } #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) if ((queryFlags & StatisticsQueryFlags.Cache) != 0) { stat.CacheEntityCount = HTTPCacheService.GetCacheEntityCount(); stat.CacheSize = HTTPCacheService.GetCacheSize(); } #endif #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR) if ((queryFlags & StatisticsQueryFlags.Cookies) != 0) { List cookies = Cookies.CookieJar.GetAll(); stat.CookieCount = cookies.Count; uint cookiesSize = 0; for (int i = 0; i < cookies.Count; ++i) cookiesSize += cookies[i].GuessSize(); stat.CookieJarSize = cookiesSize; } #endif return stat; } #endregion #region Private Functions private static void SendRequestImpl(HTTPRequest request) { ConnectionBase conn = FindOrCreateFreeConnection(request); if (conn != null) { // found a free connection: put it in the ActiveConnection list(they will be checked periodically in the OnUpdate call) if (ActiveConnections.Find((c) => c == conn) == null) ActiveConnections.Add(conn); FreeConnections.Remove(conn); request.State = HTTPRequestStates.Processing; request.Prepare(); // then start process the request conn.Process(request); } else { // If no free connection found and creation prohibited, we will put back to the queue request.State = HTTPRequestStates.Queued; RequestQueue.Add(request); } } private static string GetKeyForRequest(HTTPRequest request) { if (request.CurrentUri.IsFile) return request.CurrentUri.ToString(); // proxyUri + requestUri // HTTP and HTTPS needs different connections. return #if !BESTHTTP_DISABLE_PROXY (request.Proxy != null ? new UriBuilder(request.Proxy.Address.Scheme, request.Proxy.Address.Host, request.Proxy.Address.Port).Uri.ToString() : string.Empty) + #endif new UriBuilder(request.CurrentUri.Scheme, request.CurrentUri.Host, request.CurrentUri.Port).Uri.ToString(); } /// /// Factory method to create a concrete connection object. /// private static ConnectionBase CreateConnection(HTTPRequest request, string serverUrl) { if (request.CurrentUri.IsFile && Application.platform != RuntimePlatform.WebGLPlayer) return new FileConnection(serverUrl); #if UNITY_WEBGL && !UNITY_EDITOR return new WebGLConnection(serverUrl); #else return new HTTPConnection(serverUrl); #endif } private static ConnectionBase FindOrCreateFreeConnection(HTTPRequest request) { ConnectionBase conn = null; List connections; string serverUrl = GetKeyForRequest(request); if (Connections.TryGetValue(serverUrl, out connections)) { // count active connections int activeConnections = 0; for (int i = 0; i < connections.Count; ++i) if (connections[i].IsActive) activeConnections++; if (activeConnections <= MaxConnectionPerServer) // search for a Free connection for (int i = 0; i < connections.Count && conn == null; ++i) { var tmpConn = connections[i]; if (tmpConn != null && tmpConn.IsFree && ( #if !BESTHTTP_DISABLE_PROXY !tmpConn.HasProxy || #endif tmpConn.LastProcessedUri == null || tmpConn.LastProcessedUri.Host.Equals(request.CurrentUri.Host, StringComparison.OrdinalIgnoreCase))) conn = tmpConn; } } else Connections.Add(serverUrl, connections = new List(MaxConnectionPerServer)); // No free connection found? if (conn == null) { // Max connection reached? if (connections.Count >= MaxConnectionPerServer) return null; // if no, create a new one connections.Add(conn = CreateConnection(request, serverUrl)); } return conn; } /// /// Will return with true when there at least one request that can be processed from the RequestQueue. /// private static bool CanProcessFromQueue() { for (int i = 0; i < RequestQueue.Count; ++i) if (FindOrCreateFreeConnection(RequestQueue[i]) != null) return true; return false; } private static void RecycleConnection(ConnectionBase conn) { conn.Recycle(OnConnectionRecylced); } private static void OnConnectionRecylced(ConnectionBase conn) { lock (RecycledConnections) { RecycledConnections.Add(conn); } } #endregion #region Internal Helper Functions /// /// Will return the ConnectionBase object that processing the given request. /// internal static ConnectionBase GetConnectionWith(HTTPRequest request) { lock (Locker) { for (int i = 0; i < ActiveConnections.Count; ++i) { var connection = ActiveConnections[i]; if (connection.CurrentRequest == request) return connection; } return null; } } internal static bool RemoveFromQueue(HTTPRequest request) { return RequestQueue.Remove(request); } #if !((BESTHTTP_DISABLE_CACHING && BESTHTTP_DISABLE_COOKIES) || (UNITY_WEBGL && !UNITY_EDITOR)) /// /// Will return where the various caches should be saved. /// internal static string GetRootCacheFolder() { try { if (RootCacheFolderProvider != null) return RootCacheFolderProvider(); } catch(Exception ex) { HTTPManager.Logger.Exception("HTTPManager", "GetRootCacheFolder", ex); } #if NETFX_CORE return Windows.Storage.ApplicationData.Current.LocalFolder.Path; #else return Application.persistentDataPath; #endif } #endif #endregion #region MonoBehaviour Events (Called from HTTPUpdateDelegator) /// /// Update function that should be called regularly from a Unity event(Update, LateUpdate). Callbacks are dispatched from this function. /// public static void OnUpdate() { // We will try to acquire a lock. If it fails, we will skip this frame without calling any callback. if (System.Threading.Monitor.TryEnter(Locker)) { try { IsCallingCallbacks = true; try { for (int i = 0; i < ActiveConnections.Count; ++i) { ConnectionBase conn = ActiveConnections[i]; switch (conn.State) { case HTTPConnectionStates.Processing: conn.HandleProgressCallback(); if (conn.CurrentRequest.UseStreaming && conn.CurrentRequest.Response != null && conn.CurrentRequest.Response.HasStreamedFragments()) conn.HandleCallback(); try { if (((!conn.CurrentRequest.UseStreaming && conn.CurrentRequest.UploadStream == null) || conn.CurrentRequest.EnableTimoutForStreaming) && DateTime.UtcNow - conn.StartTime > conn.CurrentRequest.Timeout) conn.Abort(HTTPConnectionStates.TimedOut); } catch (OverflowException) { HTTPManager.Logger.Warning("HTTPManager", "TimeSpan overflow"); } break; case HTTPConnectionStates.TimedOut: // The connection is still in TimedOut state, and if we waited enough time, we will dispatch the // callback and recycle the connection try { if (DateTime.UtcNow - conn.TimedOutStart > TimeSpan.FromMilliseconds(500)) { HTTPManager.Logger.Information("HTTPManager", "Hard aborting connection because of a long waiting TimedOut state"); conn.CurrentRequest.Response = null; conn.CurrentRequest.State = HTTPRequestStates.TimedOut; conn.HandleCallback(); // this will set the connection's CurrentRequest to null RecycleConnection(conn); } } catch(OverflowException) { HTTPManager.Logger.Warning("HTTPManager", "TimeSpan overflow"); } break; case HTTPConnectionStates.Redirected: // If the server redirected us, we need to find or create a connection to the new server and send out the request again. SendRequest(conn.CurrentRequest); RecycleConnection(conn); break; case HTTPConnectionStates.WaitForRecycle: // If it's a streamed request, it's finished now conn.CurrentRequest.FinishStreaming(); // Call the callback conn.HandleCallback(); // Then recycle the connection RecycleConnection(conn); break; case HTTPConnectionStates.Upgraded: // The connection upgraded to an other protocol conn.HandleCallback(); break; case HTTPConnectionStates.WaitForProtocolShutdown: var ws = conn.CurrentRequest.Response as IProtocol; if (ws != null) ws.HandleEvents(); if (ws == null || ws.IsClosed) { conn.HandleCallback(); // After both sending and receiving a Close message, an endpoint considers the WebSocket connection closed and MUST close the underlying TCP connection. conn.Dispose(); RecycleConnection(conn); } break; case HTTPConnectionStates.AbortRequested: // Corner case: we aborted a WebSocket connection { ws = conn.CurrentRequest.Response as IProtocol; if (ws != null) { ws.HandleEvents(); if (ws.IsClosed) { conn.HandleCallback(); conn.Dispose(); RecycleConnection(conn); } } } break; case HTTPConnectionStates.Closed: // If it's a streamed request, it's finished now conn.CurrentRequest.FinishStreaming(); // Call the callback conn.HandleCallback(); // It will remove from the ActiveConnections RecycleConnection(conn); break; case HTTPConnectionStates.Free: RecycleConnection(conn); break; } } } finally { IsCallingCallbacks = false; } // Just try to grab the lock, if we can't have it we can wait another turn because it isn't // critical to do it right now. if (System.Threading.Monitor.TryEnter(RecycledConnections)) try { if (RecycledConnections.Count > 0) { for (int i = 0; i < RecycledConnections.Count; ++i) { var connection = RecycledConnections[i]; // If in a callback made a request that acquired this connection, then we will not remove it from the // active connections. if (connection.IsFree) { ActiveConnections.Remove(connection); FreeConnections.Add(connection); } } RecycledConnections.Clear(); } } finally { System.Threading.Monitor.Exit(RecycledConnections); } if (FreeConnections.Count > 0) for (int i = 0; i < FreeConnections.Count; i++) { var connection = FreeConnections[i]; if (connection.IsRemovable) { // Remove the connection from the connection reference table List connections = null; if (Connections.TryGetValue(connection.ServerAddress, out connections)) connections.Remove(connection); // Dispose the connection connection.Dispose(); FreeConnections.RemoveAt(i); i--; } } if (CanProcessFromQueue()) { // Sort the queue by priority, only if we have to if (RequestQueue.Find((req) => req.Priority != 0) != null) RequestQueue.Sort((req1, req2) => req1.Priority - req2.Priority); // Create an array from the queue and clear it. When we call the SendRequest while still no room for new connections, the same queue will be rebuilt. var queue = RequestQueue.ToArray(); RequestQueue.Clear(); for (int i = 0; i < queue.Length; ++i) SendRequest(queue[i]); } } finally { System.Threading.Monitor.Exit(Locker); } } if (heartbeats != null) heartbeats.Update(); } public static void OnQuit() { lock (Locker) { IsQuitting = true; #if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR) Caching.HTTPCacheService.SaveLibrary(); #endif #if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR) Cookies.CookieJar.Persist(); #endif AbortAll(true); OnUpdate(); } } public static void AbortAll(bool allowCallbacks = false) { lock (Locker) { var queue = RequestQueue.ToArray(); RequestQueue.Clear(); foreach (var req in queue) { // Swallow any exceptions, we are quitting anyway. try { if (!allowCallbacks) req.Callback = null; req.Abort(); } catch { } } // Close all TCP connections when the application is terminating. foreach (var kvp in Connections) { foreach (var conn in kvp.Value) { // Swallow any exceptions, we are quitting anyway. try { if (conn.CurrentRequest != null) { if (!allowCallbacks) conn.CurrentRequest.Callback = null; conn.CurrentRequest.State = HTTPRequestStates.Aborted; } conn.Abort(HTTPConnectionStates.Closed); conn.Dispose(); } catch { } } kvp.Value.Clear(); } Connections.Clear(); } } #endregion } }