#if !UNITY_WSA && !UNITY_WP8 using System; using UnityEngine; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading; using PlayFab.SharedModels; #if !DISABLE_PLAYFABCLIENT_API using PlayFab.ClientModels; #endif namespace PlayFab.Internal { public class PlayFabWebRequest : ITransportPlugin { /// /// Disable encryption certificate validation within PlayFabWebRequest using this request. /// This is not generally recommended. /// As of early 2018: /// None of the built-in Unity mechanisms validate the certificate, using .Net 3.5 equivalent runtime /// It is also not currently feasible to provide a single cross platform solution that will correctly validate a certificate. /// The Risk: /// All Unity HTTPS mechanisms are vulnerable to Man-In-The-Middle attacks. /// The only more-secure option is to define a custom CustomCertValidationHook, specifically tailored to the platforms you support, /// which validate the cert based on a list of trusted certificate providers. This list of providers must be able to update itself, as the /// base certificates for those providers will also expire and need updating on a regular basis. /// public static void SkipCertificateValidation() { var rcvc = new System.Net.Security.RemoteCertificateValidationCallback(AcceptAllCertifications); //(sender, cert, chain, ssl) => true ServicePointManager.ServerCertificateValidationCallback = rcvc; certValidationSet = true; } /// /// Provide PlayFabWebRequest with a custom ServerCertificateValidationCallback which can be used to validate the PlayFab encryption certificate. /// Please do not: /// - Hard code the current PlayFab certificate information - The PlayFab certificate updates itself on a regular schedule, and your game will fail and require a republish to fix /// - Hard code a list of static certificate authorities - Any single exported list of certificate authorities will become out of date, and have the same problem when the CA cert expires /// Real solution: /// - A mechanism where a valid certificate authority list can be securely downloaded and updated without republishing the client when existing certificates expire. /// public static System.Net.Security.RemoteCertificateValidationCallback CustomCertValidationHook { set { ServicePointManager.ServerCertificateValidationCallback = value; certValidationSet = true; } } private static readonly Queue ResultQueueTransferThread = new Queue(); private static readonly Queue ResultQueueMainThread = new Queue(); private static readonly List ActiveRequests = new List(); private static bool certValidationSet = false; private static Thread _requestQueueThread; private static readonly object _ThreadLock = new object(); private static readonly TimeSpan ThreadKillTimeout = TimeSpan.FromSeconds(60); private static DateTime _threadKillTime = DateTime.UtcNow + ThreadKillTimeout; // Kill the thread after 1 minute of inactivity private static bool _isApplicationPlaying; private static int _activeCallCount; private static string _unityVersion; private bool _isInitialized = false; public bool IsInitialized { get { return _isInitialized; } } public void Initialize() { SetupCertificates(); _isApplicationPlaying = true; _unityVersion = Application.unityVersion; _isInitialized = true; } public void OnDestroy() { _isApplicationPlaying = false; lock (ResultQueueTransferThread) { ResultQueueTransferThread.Clear(); } lock (ActiveRequests) { ActiveRequests.Clear(); } lock (_ThreadLock) { _requestQueueThread = null; } } private void SetupCertificates() { // These are performance Optimizations for HttpWebRequests. ServicePointManager.DefaultConnectionLimit = 10; ServicePointManager.Expect100Continue = false; if (!certValidationSet) { Debug.LogWarning("PlayFab API calls will likely fail because you have not set up a HttpWebRequest certificate validation mechanism"); Debug.LogWarning("Please set a validation callback into PlayFab.Internal.PlayFabWebRequest.CustomCertValidationHook, or set PlayFab.Internal.PlayFabWebRequest.SkipCertificateValidation()"); } } /// /// This disables certificate validation, if it's been activated by a customer via SkipCertificateValidation() /// private static bool AcceptAllCertifications(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { return true; } public void SimpleGetCall(string fullUrl, Action successCallback, Action errorCallback) { // This needs to be improved to use a decent thread-pool, but it can be improved invisibly later var newThread = new Thread(() => SimpleHttpsWorker("GET", fullUrl, null, successCallback, errorCallback)); newThread.Start(); } public void SimplePutCall(string fullUrl, byte[] payload, Action successCallback, Action errorCallback) { // This needs to be improved to use a decent thread-pool, but it can be improved invisibly later var newThread = new Thread(() => SimpleHttpsWorker("PUT", fullUrl, payload, successCallback, errorCallback)); newThread.Start(); } public void SimplePostCall(string fullUrl, byte[] payload, Action successCallback, Action errorCallback) { // This needs to be improved to use a decent thread-pool, but it can be improved invisibly later var newThread = new Thread(() => SimpleHttpsWorker("POST", fullUrl, payload, successCallback, errorCallback)); newThread.Start(); } private void SimpleHttpsWorker(string httpMethod, string fullUrl, byte[] payload, Action successCallback, Action errorCallback) { // This should also use a pooled HttpWebRequest object, but that too can be improved invisibly later var httpRequest = (HttpWebRequest)WebRequest.Create(fullUrl); httpRequest.UserAgent = "UnityEngine-Unity; Version: " + _unityVersion; httpRequest.Method = httpMethod; httpRequest.KeepAlive = PlayFabSettings.RequestKeepAlive; httpRequest.Timeout = PlayFabSettings.RequestTimeout; httpRequest.AllowWriteStreamBuffering = false; httpRequest.ReadWriteTimeout = PlayFabSettings.RequestTimeout; if (payload != null) { httpRequest.ContentLength = payload.LongLength; using (var stream = httpRequest.GetRequestStream()) { stream.Write(payload, 0, payload.Length); } } try { var response = httpRequest.GetResponse(); byte[] output = null; using (var responseStream = response.GetResponseStream()) { if (responseStream != null) { output = new byte[response.ContentLength]; responseStream.Read(output, 0, output.Length); } } successCallback(output); } catch (WebException webException) { try { using (var responseStream = webException.Response.GetResponseStream()) { if (responseStream != null) using (var stream = new StreamReader(responseStream)) errorCallback(stream.ReadToEnd()); } } catch (Exception e) { Debug.LogException(e); } } catch (Exception e) { Debug.LogException(e); } } public void MakeApiCall(object reqContainerObj) { CallRequestContainer reqContainer = (CallRequestContainer)reqContainerObj; reqContainer.HttpState = HttpRequestState.Idle; lock (ActiveRequests) { ActiveRequests.Insert(0, reqContainer); } ActivateThreadWorker(); } private static void ActivateThreadWorker() { lock (_ThreadLock) { if (_requestQueueThread != null) { return; } _requestQueueThread = new Thread(WorkerThreadMainLoop); _requestQueueThread.Start(); } } private static void WorkerThreadMainLoop() { try { bool active; lock (_ThreadLock) { // Kill the thread after 1 minute of inactivity _threadKillTime = DateTime.UtcNow + ThreadKillTimeout; } List localActiveRequests = new List(); do { //process active requests lock (ActiveRequests) { localActiveRequests.AddRange(ActiveRequests); ActiveRequests.Clear(); _activeCallCount = localActiveRequests.Count; } var activeCalls = localActiveRequests.Count; for (var i = activeCalls - 1; i >= 0; i--) // We must iterate backwards, because we remove at index i in some cases { switch (localActiveRequests[i].HttpState) { case HttpRequestState.Error: localActiveRequests.RemoveAt(i); break; case HttpRequestState.Idle: Post(localActiveRequests[i]); break; case HttpRequestState.Sent: if (!localActiveRequests[i].CalledGetResponse) { // Else we'll GetResponse try again next tick localActiveRequests[i].HttpRequest.GetResponseAsync(); localActiveRequests[i].CalledGetResponse = true; } else if (localActiveRequests[i].HttpRequest.HaveResponse) ProcessHttpResponse(localActiveRequests[i]); break; case HttpRequestState.Received: ProcessJsonResponse(localActiveRequests[i]); localActiveRequests.RemoveAt(i); break; } } #region Expire Thread. // Check if we've been inactive lock (_ThreadLock) { var now = DateTime.UtcNow; if (activeCalls > 0 && _isApplicationPlaying) { // Still active, reset the _threadKillTime _threadKillTime = now + ThreadKillTimeout; } // Kill the thread after 1 minute of inactivity active = now <= _threadKillTime; if (!active) { _requestQueueThread = null; } // This thread will be stopped, so null this now, inside lock (_threadLock) } #endregion Thread.Sleep(1); } while (active); } catch (Exception e) { Debug.LogException(e); _requestQueueThread = null; } } private static void Post(CallRequestContainer reqContainer) { try { reqContainer.HttpRequest = (HttpWebRequest)WebRequest.Create(reqContainer.FullUrl); reqContainer.HttpRequest.UserAgent = "UnityEngine-Unity; Version: " + _unityVersion; reqContainer.HttpRequest.SendChunked = false; // Prevents hitting a proxy if no proxy is available. TODO: Add support for proxy's. reqContainer.HttpRequest.Proxy = null; foreach (var pair in reqContainer.RequestHeaders) reqContainer.HttpRequest.Headers.Add(pair.Key, pair.Value); reqContainer.HttpRequest.ContentType = "application/json"; reqContainer.HttpRequest.Method = "POST"; reqContainer.HttpRequest.KeepAlive = PlayFabSettings.RequestKeepAlive; reqContainer.HttpRequest.Timeout = PlayFabSettings.RequestTimeout; reqContainer.HttpRequest.AllowWriteStreamBuffering = false; reqContainer.HttpRequest.Proxy = null; reqContainer.HttpRequest.ContentLength = reqContainer.Payload.LongLength; reqContainer.HttpRequest.ReadWriteTimeout = PlayFabSettings.RequestTimeout; //Debug.Log("Get Stream"); // Get Request Stream and send data in the body. using (var stream = reqContainer.HttpRequest.GetRequestStream()) { //Debug.Log("Post Stream"); stream.Write(reqContainer.Payload, 0, reqContainer.Payload.Length); //Debug.Log("After Post stream"); } reqContainer.HttpState = HttpRequestState.Sent; } catch (WebException e) { reqContainer.JsonResponse = ResponseToString(e.Response) ?? e.Status + ": WebException making http request to: " + reqContainer.FullUrl; var enhancedError = new WebException(reqContainer.JsonResponse, e); Debug.LogException(enhancedError); QueueRequestError(reqContainer); } catch (Exception e) { reqContainer.JsonResponse = "Unhandled exception in Post : " + reqContainer.FullUrl; var enhancedError = new Exception(reqContainer.JsonResponse, e); Debug.LogException(enhancedError); QueueRequestError(reqContainer); } } private static void ProcessHttpResponse(CallRequestContainer reqContainer) { try { #if PLAYFAB_REQUEST_TIMING reqContainer.Timing.WorkerRequestMs = (int)reqContainer.Stopwatch.ElapsedMilliseconds; #endif // Get and check the response var httpResponse = (HttpWebResponse)reqContainer.HttpRequest.GetResponse(); if (httpResponse.StatusCode == HttpStatusCode.OK) { reqContainer.JsonResponse = ResponseToString(httpResponse); } if (httpResponse.StatusCode != HttpStatusCode.OK || string.IsNullOrEmpty(reqContainer.JsonResponse)) { reqContainer.JsonResponse = reqContainer.JsonResponse ?? "No response from server"; QueueRequestError(reqContainer); return; } else { // Response Recieved Successfully, now process. } reqContainer.HttpState = HttpRequestState.Received; } catch (Exception e) { var msg = "Unhandled exception in ProcessHttpResponse : " + reqContainer.FullUrl; reqContainer.JsonResponse = reqContainer.JsonResponse ?? msg; var enhancedError = new Exception(msg, e); Debug.LogException(enhancedError); QueueRequestError(reqContainer); } } /// /// Set the reqContainer into an error state, and queue it to invoke the ErrorCallback for that request /// private static void QueueRequestError(CallRequestContainer reqContainer) { reqContainer.Error = PlayFabHttp.GeneratePlayFabError(reqContainer.ApiEndpoint, reqContainer.JsonResponse, reqContainer.CustomData); // Decode the server-json error reqContainer.HttpState = HttpRequestState.Error; lock (ResultQueueTransferThread) { //Queue The result callbacks to run on the main thread. ResultQueueTransferThread.Enqueue(() => { PlayFabHttp.SendErrorEvent(reqContainer.ApiRequest, reqContainer.Error); if (reqContainer.ErrorCallback != null) reqContainer.ErrorCallback(reqContainer.Error); }); } } private static void ProcessJsonResponse(CallRequestContainer reqContainer) { try { var serializer = PluginManager.GetPlugin(PluginContract.PlayFab_Serializer); var httpResult = serializer.DeserializeObject(reqContainer.JsonResponse); #if PLAYFAB_REQUEST_TIMING reqContainer.Timing.WorkerRequestMs = (int)reqContainer.Stopwatch.ElapsedMilliseconds; #endif //This would happen if playfab returned a 500 internal server error or a bad json response. if (httpResult == null || httpResult.code != 200) { QueueRequestError(reqContainer); return; } reqContainer.JsonResponse = serializer.SerializeObject(httpResult.data); reqContainer.DeserializeResultJson(); // Assigns Result with a properly typed object reqContainer.ApiResult.Request = reqContainer.ApiRequest; reqContainer.ApiResult.CustomData = reqContainer.CustomData; if(_isApplicationPlaying) { PlayFabHttp.instance.OnPlayFabApiResult(reqContainer); } #if !DISABLE_PLAYFABCLIENT_API lock (ResultQueueTransferThread) { ResultQueueTransferThread.Enqueue(() => { PlayFabDeviceUtil.OnPlayFabLogin(reqContainer.ApiResult, reqContainer.settings, reqContainer.instanceApi); }); } #endif lock (ResultQueueTransferThread) { //Queue The result callbacks to run on the main thread. ResultQueueTransferThread.Enqueue(() => { #if PLAYFAB_REQUEST_TIMING reqContainer.Stopwatch.Stop(); reqContainer.Timing.MainThreadRequestMs = (int)reqContainer.Stopwatch.ElapsedMilliseconds; PlayFabHttp.SendRequestTiming(reqContainer.Timing); #endif try { PlayFabHttp.SendEvent(reqContainer.ApiEndpoint, reqContainer.ApiRequest, reqContainer.ApiResult, ApiProcessingEventType.Post); reqContainer.InvokeSuccessCallback(); } catch (Exception e) { Debug.LogException(e); // Log the user's callback exception back to them without halting PlayFabHttp } }); } } catch (Exception e) { var msg = "Unhandled exception in ProcessJsonResponse : " + reqContainer.FullUrl; reqContainer.JsonResponse = reqContainer.JsonResponse ?? msg; var enhancedError = new Exception(msg, e); Debug.LogException(enhancedError); QueueRequestError(reqContainer); } } public void Update() { lock (ResultQueueTransferThread) { while (ResultQueueTransferThread.Count > 0) { var actionToQueue = ResultQueueTransferThread.Dequeue(); ResultQueueMainThread.Enqueue(actionToQueue); } } while (ResultQueueMainThread.Count > 0) { var finishedRequest = ResultQueueMainThread.Dequeue(); finishedRequest(); } } private static string ResponseToString(WebResponse webResponse) { if (webResponse == null) return null; try { using (var responseStream = webResponse.GetResponseStream()) { if (responseStream == null) return null; using (var stream = new StreamReader(responseStream)) { return stream.ReadToEnd(); } } } catch (WebException webException) { try { using (var responseStream = webException.Response.GetResponseStream()) { if (responseStream == null) return null; using (var stream = new StreamReader(responseStream)) { return stream.ReadToEnd(); } } } catch (Exception e) { Debug.LogException(e); return null; } } catch (Exception e) { Debug.LogException(e); return null; } } public int GetPendingMessages() { var count = 0; lock (ActiveRequests) count += ActiveRequests.Count + _activeCallCount; lock (ResultQueueTransferThread) count += ResultQueueTransferThread.Count; return count; } } } #endif