You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
545 lines
23 KiB
C#
545 lines
23 KiB
C#
#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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static void SkipCertificateValidation()
|
|
{
|
|
var rcvc = new System.Net.Security.RemoteCertificateValidationCallback(AcceptAllCertifications); //(sender, cert, chain, ssl) => true
|
|
ServicePointManager.ServerCertificateValidationCallback = rcvc;
|
|
certValidationSet = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static System.Net.Security.RemoteCertificateValidationCallback CustomCertValidationHook
|
|
{
|
|
set
|
|
{
|
|
ServicePointManager.ServerCertificateValidationCallback = value;
|
|
certValidationSet = true;
|
|
}
|
|
}
|
|
|
|
private static readonly Queue<Action> ResultQueueTransferThread = new Queue<Action>();
|
|
private static readonly Queue<Action> ResultQueueMainThread = new Queue<Action>();
|
|
private static readonly List<CallRequestContainer> ActiveRequests = new List<CallRequestContainer>();
|
|
|
|
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()");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This disables certificate validation, if it's been activated by a customer via SkipCertificateValidation()
|
|
/// </summary>
|
|
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<byte[]> successCallback, Action<string> 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<byte[]> successCallback, Action<string> 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<byte[]> successCallback, Action<string> 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<byte[]> successCallback, Action<string> 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<CallRequestContainer> localActiveRequests = new List<CallRequestContainer>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the reqContainer into an error state, and queue it to invoke the ErrorCallback for that request
|
|
/// </summary>
|
|
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<ISerializerPlugin>(PluginContract.PlayFab_Serializer);
|
|
var httpResult = serializer.DeserializeObject<HttpResponseObject>(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
|