fix(all): stabilize login and proxy-aware game startup
This commit is contained in:
@@ -228,6 +228,9 @@ internal static class LoginController
|
||||
|
||||
private static void ShowLoginPanel(object introUI)
|
||||
{
|
||||
// The official manual-login branch cancels the 10-second startup watchdog.
|
||||
// Browser authentication waits for the user and must not retain that timer.
|
||||
CancelMaintenanceTimeout.Invoke(introUI, null);
|
||||
AccessTokens.Clear();
|
||||
LoginInProgress = false;
|
||||
EstablishedGameSession = false;
|
||||
@@ -269,12 +272,15 @@ internal static class LoginController
|
||||
|
||||
private static IEnumerator DeviceLogin(object introUI, string provider)
|
||||
{
|
||||
string endpoint = new Uri(ServerRoot, "auth/device").AbsoluteUri;
|
||||
int generation = Volatile.Read(ref RecoveryGeneration);
|
||||
Uri origin = ServerRoot;
|
||||
string endpoint = new Uri(origin, "auth/device").AbsoluteUri;
|
||||
byte[] body = Encoding.UTF8.GetBytes(JsonUtility.ToJson(new DeviceRequest { provider = provider }));
|
||||
{
|
||||
ControlProbeResult request = null;
|
||||
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, body, null, result => request = result);
|
||||
if (request == null) yield break;
|
||||
if (!IsCurrentLogin(generation, origin)) { request.Body = null; yield break; }
|
||||
if (!request.Success)
|
||||
{
|
||||
LoginInProgress = false;
|
||||
@@ -299,24 +305,24 @@ internal static class LoginController
|
||||
yield break;
|
||||
}
|
||||
Application.OpenURL(start.start_url);
|
||||
yield return PollDevice(introUI, start);
|
||||
yield return PollDevice(introUI, start, generation, origin);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator PollDevice(object introUI, DeviceStart start)
|
||||
private static IEnumerator PollDevice(object introUI, DeviceStart start, int generation, Uri origin)
|
||||
{
|
||||
int generation = Volatile.Read(ref RecoveryGeneration);
|
||||
int delay = Math.Max(1, start.poll_interval);
|
||||
float deadline = Time.realtimeSinceStartup + Math.Max(30, start.expires_in);
|
||||
string endpoint = new Uri(ServerRoot, "auth/device/" + Uri.EscapeDataString(start.transaction_id) + "/poll").AbsoluteUri;
|
||||
string endpoint = new Uri(origin, "auth/device/" + Uri.EscapeDataString(start.transaction_id) + "/poll").AbsoluteUri;
|
||||
while (Time.realtimeSinceStartup < deadline)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
if (generation != Volatile.Read(ref RecoveryGeneration)) yield break;
|
||||
if (!IsCurrentLogin(generation, origin)) { start.device_secret = null; yield break; }
|
||||
{
|
||||
ControlProbeResult request = null;
|
||||
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, Array.Empty<byte>(), "Device " + start.device_secret, result => request = result);
|
||||
if (request == null) yield break;
|
||||
if (!IsCurrentLogin(generation, origin)) { request.Body = null; start.device_secret = null; yield break; }
|
||||
if (request.StatusCode == 202)
|
||||
{
|
||||
continue;
|
||||
@@ -345,7 +351,7 @@ internal static class LoginController
|
||||
Log?.LogError("Login transaction returned incomplete credentials");
|
||||
yield break;
|
||||
}
|
||||
CompleteInteractiveLogin(introUI, result);
|
||||
CompleteInteractiveLogin(introUI, result, generation, origin);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -353,8 +359,17 @@ internal static class LoginController
|
||||
Log?.LogError("Login transaction expired");
|
||||
}
|
||||
|
||||
private static void CompleteInteractiveLogin(object introUI, TokenResult result)
|
||||
private static bool IsCurrentLogin(int generation, Uri origin) =>
|
||||
generation == Volatile.Read(ref RecoveryGeneration) && origin != null && ServerRoot != null && SameOrigin(origin, ServerRoot);
|
||||
|
||||
private static void CompleteInteractiveLogin(object introUI, TokenResult result, int generation, Uri origin)
|
||||
{
|
||||
if (!IsCurrentLogin(generation, origin))
|
||||
{
|
||||
result.access_token = null;
|
||||
result.refresh_token = null;
|
||||
return;
|
||||
}
|
||||
if (!RefreshCredentials.IsSupported)
|
||||
{
|
||||
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
||||
@@ -370,6 +385,12 @@ internal static class LoginController
|
||||
}
|
||||
Action confirmed = delegate
|
||||
{
|
||||
if (!IsCurrentLogin(generation, origin))
|
||||
{
|
||||
result.access_token = null;
|
||||
result.refresh_token = null;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
bool autoLogin = PlayerPrefs.GetInt("StandaloneAutoLogin", 0) != 0;
|
||||
@@ -483,6 +504,8 @@ internal static class LoginController
|
||||
bool credentialRejected = (request.StatusCode == 401 || request.StatusCode == 409) && request.RefreshInvalid;
|
||||
if (credentialRejected)
|
||||
{
|
||||
Log?.LogWarning("Automatic-login credential rejected: HTTP=" + request.StatusCode +
|
||||
", refreshInvalid=" + request.RefreshInvalid + ", classification=invalid-credential");
|
||||
refreshToken = null;
|
||||
attemptID = null;
|
||||
ClearSavedLogin();
|
||||
@@ -576,6 +599,7 @@ internal static class LoginController
|
||||
try
|
||||
{
|
||||
ContinueMaintenance = true;
|
||||
Log?.LogInfo("Authenticated login is requesting maintenance information: automatic=" + automatic);
|
||||
SendMaintenance.Invoke(introUI, new object[] { automatic });
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -14,6 +14,7 @@ internal static class LoginRuntime
|
||||
internal static ManualLogSource Log;
|
||||
internal static MethodInfo SetIntroState;
|
||||
internal static MethodInfo SendMaintenance;
|
||||
internal static MethodInfo CancelMaintenanceTimeout;
|
||||
internal static Uri ServerRoot;
|
||||
internal static MethodInfo OpenPCLoginPopup;
|
||||
internal static MethodInfo EnterGame;
|
||||
|
||||
@@ -31,6 +31,9 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
try
|
||||
{
|
||||
Log = Logger;
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UNITY_PROXYSERVER")) &&
|
||||
!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UNITY_NOPROXY")))
|
||||
Logger.LogInfo("Game loopback transport uses launch-time Unity proxy bypass");
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => DisposeGameRelay();
|
||||
Game.Validate(typeof(Plugin).Assembly, Bd2Build.Versions.Game, message => Logger.LogInfo(message));
|
||||
EnsureRecoveryHost();
|
||||
@@ -49,6 +52,8 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
throw new MissingMethodException("IntroUI.Awake() was not found (client version mismatch)");
|
||||
}
|
||||
SendMaintenance = Game.Method<IntroUI>(ui => ui.SendMaintenanceInfo(false));
|
||||
CancelMaintenanceTimeout = introUI.GetGameMethod(
|
||||
"CancelMaintenanceTimeout", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
SetIntroState = introUI.GetGameMethod("SetIntroState", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
EnterGame = introUI.GetGameMethod(
|
||||
"Enter",
|
||||
@@ -72,7 +77,7 @@ public sealed class Plugin : BaseUnityPlugin
|
||||
MethodInfo exponentialBackOff = networkManager?.GetGameMethod(
|
||||
"ExponetialBackOff",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (SendMaintenance == null || SetIntroState == null || OpenPCLoginPopup == null ||
|
||||
if (SendMaintenance == null || CancelMaintenanceTimeout == null || CancelMaintenanceTimeout.ReturnType != typeof(void) || SetIntroState == null || OpenPCLoginPopup == null ||
|
||||
accessTokenGetter == null || clearPCLocalData == null || disposeWebRequest == null ||
|
||||
clientNetworkError == null || exponentialBackOff == null || EnterGame == null)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class SecureGameRelay : IDisposable
|
||||
{
|
||||
try { using (client) await Serve(client).ConfigureAwait(false); }
|
||||
catch (Exception ex) when (ex is IOException || ex is SocketException || ex is OperationCanceledException || ex is InvalidDataException)
|
||||
{ /* Close failed transport: the game's existing ConnectionError recovery handles it. */ }
|
||||
{ log?.LogWarning("Native game relay connection failed: " + ex.GetType().Name); }
|
||||
catch (Exception ex) { log?.LogWarning("Native game transport failed: " + ex.GetType().Name); }
|
||||
finally { slots.Release(); }
|
||||
});
|
||||
@@ -108,6 +108,8 @@ internal sealed class SecureGameRelay : IDisposable
|
||||
throw new InvalidDataException("Invalid relay request");
|
||||
if (!Uri.TryCreate(local, start[1], out Uri target) || !TryResolve(target, out Uri remote))
|
||||
throw new InvalidDataException("Invalid relay target");
|
||||
bool maintenance = remote.AbsolutePath.Equals("/game/MaintenanceInfo", StringComparison.Ordinal);
|
||||
if (maintenance) log?.LogInfo("Game relay received MaintenanceInfo request");
|
||||
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
int length = 0;
|
||||
bool lengthSeen = false;
|
||||
@@ -139,6 +141,9 @@ internal sealed class SecureGameRelay : IDisposable
|
||||
}
|
||||
response = await PlatformControlHttp.Send(remote, start[0], body, null, 4, lifetime.Token,
|
||||
lifetime.Token, 64 * 1024 * 1024, headers).ConfigureAwait(false);
|
||||
if (maintenance || response.StatusCode == 0)
|
||||
log?.LogInfo("Game relay upstream result: path=" + remote.AbsolutePath + " status=" + response.StatusCode +
|
||||
" error=" + (response.Error ?? "none"));
|
||||
if (response.StatusCode == 0 || response.Data == null) throw new IOException("Native game transport unavailable");
|
||||
StringBuilder output = new StringBuilder("HTTP/1.1 ").Append(response.StatusCode.ToString(CultureInfo.InvariantCulture)).Append(" Response\r\nConnection: close\r\nCache-Control: no-store\r\n");
|
||||
foreach (KeyValuePair<string, string> item in response.Headers)
|
||||
|
||||
@@ -225,6 +225,39 @@ internal static class SessionRecovery
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
const BindingFlags diagnosticFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
object packet = __0?.GetType().GetGameProperty("PacketData", diagnosticFlags)?.GetValue(__0, null);
|
||||
string path = packet?.GetType().GetGameProperty("SendPath", diagnosticFlags)?.GetValue(packet, null) as string;
|
||||
if (path == "MaintenanceInfo")
|
||||
{
|
||||
string server = packet?.GetType().GetGameField("RequestServerURL", diagnosticFlags)?.GetValue(packet) as string;
|
||||
string endpoint = Uri.TryCreate(server, UriKind.Absolute, out Uri parsed) ? parsed.GetLeftPart(UriPartial.Authority) + parsed.AbsolutePath : "invalid";
|
||||
string message = __0?.GetType().GetGameProperty("Message", diagnosticFlags)?.GetValue(__0, null) as string ?? string.Empty;
|
||||
string category = message.IndexOf("tim", StringComparison.OrdinalIgnoreCase) >= 0 ? "Timeout" :
|
||||
message.IndexOf("cert", StringComparison.OrdinalIgnoreCase) >= 0 || message.IndexOf("TLS", StringComparison.OrdinalIgnoreCase) >= 0 || message.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 ? "TLS" : "Other";
|
||||
Log?.LogDebug("Maintenance backoff: endpoint=" + endpoint + ", category=" + category + ", observedRequests=" + GameRequests.Count);
|
||||
foreach (WeakReference<UnityWebRequest> reference in GameRequests)
|
||||
{
|
||||
if (!reference.TryGetTarget(out UnityWebRequest request) || !request.isDone) continue;
|
||||
if (!TryGetOwnedRequestUri(request.url, out Uri original) || original.AbsolutePath != "/game/MaintenanceInfo") continue;
|
||||
string error = request.error ?? string.Empty;
|
||||
string safeError = error.IndexOf("Insecure", StringComparison.OrdinalIgnoreCase) >= 0 ? "InsecureConnectionBlocked" :
|
||||
error.IndexOf("connect", StringComparison.OrdinalIgnoreCase) >= 0 ? "ConnectionFailure" :
|
||||
error.IndexOf("tim", StringComparison.OrdinalIgnoreCase) >= 0 ? "Timeout" :
|
||||
error.IndexOf("HTTP", StringComparison.OrdinalIgnoreCase) >= 0 ? "HttpFailure" :
|
||||
error.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 || error.IndexOf("cert", StringComparison.OrdinalIgnoreCase) >= 0 ? "TlsFailure" : "Unclassified";
|
||||
Log?.LogWarning("Maintenance completed transport: result=" + request.result + ", status=" + request.responseCode +
|
||||
", error=" + safeError + ", errorLength=" + error.Length);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.LogDebug("Maintenance transport diagnostic unavailable: " + ex.GetType().Name);
|
||||
}
|
||||
if (ServerRoot == null || !IsConfiguredServerFailure(__0) || !IsTransportFailure(__0))
|
||||
{
|
||||
return true;
|
||||
@@ -443,8 +476,10 @@ internal static class SessionRecovery
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool SuppressNetworkErrorDuringRecovery()
|
||||
internal static bool SuppressNetworkErrorDuringRecovery(object __0, object __1)
|
||||
{
|
||||
Log?.LogWarning("Client network error: type=" + __0 + ", code=" + __1 +
|
||||
", recovering=" + (Volatile.Read(ref SessionRecoveryInProgress) != 0));
|
||||
return Volatile.Read(ref SessionRecoveryInProgress) == 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user