fix(all): stabilize login and proxy-aware game startup
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unity reads these variables before managed plugins can run.
|
||||||
|
func gameProxyEnvironment(environment []string, systemProxy string) []string {
|
||||||
|
result := append([]string(nil), environment...)
|
||||||
|
lookup := func(name string) (int, string) {
|
||||||
|
for index, entry := range result {
|
||||||
|
key, value, found := strings.Cut(entry, "=")
|
||||||
|
if found && strings.EqualFold(key, name) {
|
||||||
|
return index, value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, ""
|
||||||
|
}
|
||||||
|
if index, _ := lookup("UNITY_PROXYSERVER"); index < 0 && validUnityProxy(systemProxy) {
|
||||||
|
result = append(result, "UNITY_PROXYSERVER="+systemProxy)
|
||||||
|
}
|
||||||
|
index, bypass := lookup("UNITY_NOPROXY")
|
||||||
|
parts := strings.FieldsFunc(bypass, func(character rune) bool { return character == ',' || character == ';' })
|
||||||
|
for _, local := range []string{"localhost", "127.0.0.1", "::1"} {
|
||||||
|
present := false
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(part), local) {
|
||||||
|
present = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !present {
|
||||||
|
parts = append(parts, local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry := "UNITY_NOPROXY=" + strings.Join(parts, ",")
|
||||||
|
if index >= 0 {
|
||||||
|
result[index] = entry
|
||||||
|
} else {
|
||||||
|
result = append(result, entry)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func validUnityProxy(proxy string) bool {
|
||||||
|
parsed, err := url.Parse(proxy)
|
||||||
|
if err != nil || parsed.Scheme != "http" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host, port, err := net.SplitHostPort(parsed.Host)
|
||||||
|
if err != nil || host == "" || port == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err = net.LookupPort("tcp", port)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unity accepts one proxy, so per-scheme configurations must agree.
|
||||||
|
func sharedWindowsProxy(raw string) string {
|
||||||
|
if !strings.Contains(raw, "=") {
|
||||||
|
proxy := "http://" + strings.TrimSpace(raw)
|
||||||
|
if validUnityProxy(proxy) {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var httpProxy, httpsProxy string
|
||||||
|
for _, entry := range strings.Split(raw, ";") {
|
||||||
|
key, value, found := strings.Cut(strings.TrimSpace(entry), "=")
|
||||||
|
if !found {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "http":
|
||||||
|
httpProxy = value
|
||||||
|
case "https":
|
||||||
|
httpsProxy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if httpProxy == "" || !strings.EqualFold(httpProxy, httpsProxy) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
proxy := "http://" + httpProxy
|
||||||
|
if validUnityProxy(proxy) {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func systemGameProxy() string {
|
||||||
|
output, err := exec.Command("/usr/sbin/scutil", "--proxy").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
values := map[string]string{}
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
key, value, found := strings.Cut(strings.TrimSpace(line), " : ")
|
||||||
|
if found {
|
||||||
|
values[key] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if values["ProxyAutoConfigEnable"] == "1" || values["ProxyAutoDiscoveryEnable"] == "1" ||
|
||||||
|
values["HTTPEnable"] != "1" || values["HTTPSEnable"] != "1" ||
|
||||||
|
values["HTTPProxy"] != values["HTTPSProxy"] || values["HTTPPort"] != values["HTTPSPort"] {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(values["HTTPPort"])
|
||||||
|
if err != nil || port < 1 || port > 65535 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
proxy := "http://" + net.JoinHostPort(values["HTTPProxy"], strconv.Itoa(port))
|
||||||
|
if validUnityProxy(proxy) {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGameProxyEnvironmentPreservesExplicitAndIsIdempotent(t *testing.T) {
|
||||||
|
input := []string{"PATH=kept", "UNITY_PROXYSERVER=http://explicit:8080", "UNITY_NOPROXY=example.org;localhost"}
|
||||||
|
got := gameProxyEnvironment(input, "http://system:8888")
|
||||||
|
if !reflect.DeepEqual(got, gameProxyEnvironment(got, "http://other:9999")) {
|
||||||
|
t.Fatal("environment is not idempotent")
|
||||||
|
}
|
||||||
|
if got[1] != input[1] || input[2] != "UNITY_NOPROXY=example.org;localhost" {
|
||||||
|
t.Fatal("explicit environment changed or input mutated")
|
||||||
|
}
|
||||||
|
if !strings.Contains(got[2], "127.0.0.1") || !strings.Contains(got[2], "::1") {
|
||||||
|
t.Fatal("loopback bypass missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedWindowsProxy(t *testing.T) {
|
||||||
|
for _, test := range []struct{ input, want string }{
|
||||||
|
{"127.0.0.1:12451", "http://127.0.0.1:12451"},
|
||||||
|
{"http=proxy:8080;https=proxy:8080", "http://proxy:8080"},
|
||||||
|
{"http=proxy:8080;https=other:8080", ""},
|
||||||
|
{"https=proxy:8080", ""}, {"user:password@proxy:8080", ""}, {"proxy:99999", ""},
|
||||||
|
} {
|
||||||
|
if got := sharedWindowsProxy(test.input); got != test.want {
|
||||||
|
t.Errorf("proxy configuration result mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProxyEnvironmentIgnoresInvalidSystemProxy(t *testing.T) {
|
||||||
|
for _, proxy := range []string{"", "https://proxy:443", "http://user:password@proxy:8080", "http://proxy:8080/path"} {
|
||||||
|
for _, entry := range gameProxyEnvironment(nil, proxy) {
|
||||||
|
if strings.HasPrefix(entry, "UNITY_PROXYSERVER=") {
|
||||||
|
t.Fatal("invalid system proxy accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func systemGameProxy() string {
|
||||||
|
var config struct {
|
||||||
|
AutoDetect int32
|
||||||
|
AutoConfigURL, Proxy, Bypass *uint16
|
||||||
|
}
|
||||||
|
procedure := syscall.NewLazyDLL("winhttp.dll").NewProc("WinHttpGetIEProxyConfigForCurrentUser")
|
||||||
|
globalFree := syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalFree")
|
||||||
|
result, _, _ := procedure.Call(uintptr(unsafe.Pointer(&config)))
|
||||||
|
defer func() {
|
||||||
|
for _, pointer := range []*uint16{config.AutoConfigURL, config.Proxy, config.Bypass} {
|
||||||
|
if pointer != nil {
|
||||||
|
globalFree.Call(uintptr(unsafe.Pointer(pointer)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if result == 0 || config.AutoDetect != 0 || config.AutoConfigURL != nil || config.Proxy == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sharedWindowsProxy(windows.UTF16PtrToString(config.Proxy))
|
||||||
|
}
|
||||||
@@ -25,6 +25,16 @@ func launchGame(target string) error {
|
|||||||
if err := exec.Command("pgrep", "-x", "BrownDust II").Run(); err == nil {
|
if err := exec.Command("pgrep", "-x", "BrownDust II").Run(); err == nil {
|
||||||
return errGameAlreadyRunning
|
return errGameAlreadyRunning
|
||||||
}
|
}
|
||||||
args := append([]string{target, "--args"}, gameLaunchArguments()...)
|
// LaunchServices does not inherit open's environment; --env explicitly
|
||||||
|
// supplies the Unity variables to the newly launched application.
|
||||||
|
args := []string{target}
|
||||||
|
for _, entry := range gameProxyEnvironment(os.Environ(), systemGameProxy()) {
|
||||||
|
key, _, found := strings.Cut(entry, "=")
|
||||||
|
if found && (strings.EqualFold(key, "UNITY_PROXYSERVER") || strings.EqualFold(key, "UNITY_NOPROXY")) {
|
||||||
|
args = append(args, "--env", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = append(args, "--args")
|
||||||
|
args = append(args, gameLaunchArguments()...)
|
||||||
return exec.Command("open", args...).Start()
|
return exec.Command("open", args...).Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -79,6 +80,7 @@ func launchGame(target string) error {
|
|||||||
}
|
}
|
||||||
command := visibleCommand(target, gameLaunchArguments()...)
|
command := visibleCommand(target, gameLaunchArguments()...)
|
||||||
command.Dir = filepath.Dir(target)
|
command.Dir = filepath.Dir(target)
|
||||||
|
command.Env = gameProxyEnvironment(os.Environ(), systemGameProxy())
|
||||||
if err := command.Start(); err != nil {
|
if err := command.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ internal static class LoginController
|
|||||||
|
|
||||||
private static void ShowLoginPanel(object introUI)
|
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();
|
AccessTokens.Clear();
|
||||||
LoginInProgress = false;
|
LoginInProgress = false;
|
||||||
EstablishedGameSession = false;
|
EstablishedGameSession = false;
|
||||||
@@ -269,12 +272,15 @@ internal static class LoginController
|
|||||||
|
|
||||||
private static IEnumerator DeviceLogin(object introUI, string provider)
|
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 }));
|
byte[] body = Encoding.UTF8.GetBytes(JsonUtility.ToJson(new DeviceRequest { provider = provider }));
|
||||||
{
|
{
|
||||||
ControlProbeResult request = null;
|
ControlProbeResult request = null;
|
||||||
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, body, null, result => request = result);
|
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, body, null, result => request = result);
|
||||||
if (request == null) yield break;
|
if (request == null) yield break;
|
||||||
|
if (!IsCurrentLogin(generation, origin)) { request.Body = null; yield break; }
|
||||||
if (!request.Success)
|
if (!request.Success)
|
||||||
{
|
{
|
||||||
LoginInProgress = false;
|
LoginInProgress = false;
|
||||||
@@ -299,24 +305,24 @@ internal static class LoginController
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
Application.OpenURL(start.start_url);
|
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);
|
int delay = Math.Max(1, start.poll_interval);
|
||||||
float deadline = Time.realtimeSinceStartup + Math.Max(30, start.expires_in);
|
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)
|
while (Time.realtimeSinceStartup < deadline)
|
||||||
{
|
{
|
||||||
yield return new WaitForSecondsRealtime(delay);
|
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;
|
ControlProbeResult request = null;
|
||||||
yield return AuthRequest(introUI, new Uri(endpoint), UnityWebRequest.kHttpVerbPOST, Array.Empty<byte>(), "Device " + start.device_secret, result => request = result);
|
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 (request == null) yield break;
|
||||||
|
if (!IsCurrentLogin(generation, origin)) { request.Body = null; start.device_secret = null; yield break; }
|
||||||
if (request.StatusCode == 202)
|
if (request.StatusCode == 202)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -345,7 +351,7 @@ internal static class LoginController
|
|||||||
Log?.LogError("Login transaction returned incomplete credentials");
|
Log?.LogError("Login transaction returned incomplete credentials");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
CompleteInteractiveLogin(introUI, result);
|
CompleteInteractiveLogin(introUI, result, generation, origin);
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,8 +359,17 @@ internal static class LoginController
|
|||||||
Log?.LogError("Login transaction expired");
|
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)
|
if (!RefreshCredentials.IsSupported)
|
||||||
{
|
{
|
||||||
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
AccessTokens.Set(result.access_token, NormalizedServerOrigin(), result.provider, result.access_expires_in);
|
||||||
@@ -370,6 +385,12 @@ internal static class LoginController
|
|||||||
}
|
}
|
||||||
Action confirmed = delegate
|
Action confirmed = delegate
|
||||||
{
|
{
|
||||||
|
if (!IsCurrentLogin(generation, origin))
|
||||||
|
{
|
||||||
|
result.access_token = null;
|
||||||
|
result.refresh_token = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool autoLogin = PlayerPrefs.GetInt("StandaloneAutoLogin", 0) != 0;
|
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;
|
bool credentialRejected = (request.StatusCode == 401 || request.StatusCode == 409) && request.RefreshInvalid;
|
||||||
if (credentialRejected)
|
if (credentialRejected)
|
||||||
{
|
{
|
||||||
|
Log?.LogWarning("Automatic-login credential rejected: HTTP=" + request.StatusCode +
|
||||||
|
", refreshInvalid=" + request.RefreshInvalid + ", classification=invalid-credential");
|
||||||
refreshToken = null;
|
refreshToken = null;
|
||||||
attemptID = null;
|
attemptID = null;
|
||||||
ClearSavedLogin();
|
ClearSavedLogin();
|
||||||
@@ -576,6 +599,7 @@ internal static class LoginController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ContinueMaintenance = true;
|
ContinueMaintenance = true;
|
||||||
|
Log?.LogInfo("Authenticated login is requesting maintenance information: automatic=" + automatic);
|
||||||
SendMaintenance.Invoke(introUI, new object[] { automatic });
|
SendMaintenance.Invoke(introUI, new object[] { automatic });
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ internal static class LoginRuntime
|
|||||||
internal static ManualLogSource Log;
|
internal static ManualLogSource Log;
|
||||||
internal static MethodInfo SetIntroState;
|
internal static MethodInfo SetIntroState;
|
||||||
internal static MethodInfo SendMaintenance;
|
internal static MethodInfo SendMaintenance;
|
||||||
|
internal static MethodInfo CancelMaintenanceTimeout;
|
||||||
internal static Uri ServerRoot;
|
internal static Uri ServerRoot;
|
||||||
internal static MethodInfo OpenPCLoginPopup;
|
internal static MethodInfo OpenPCLoginPopup;
|
||||||
internal static MethodInfo EnterGame;
|
internal static MethodInfo EnterGame;
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public sealed class Plugin : BaseUnityPlugin
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Log = Logger;
|
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();
|
AppDomain.CurrentDomain.ProcessExit += (_, _) => DisposeGameRelay();
|
||||||
Game.Validate(typeof(Plugin).Assembly, Bd2Build.Versions.Game, message => Logger.LogInfo(message));
|
Game.Validate(typeof(Plugin).Assembly, Bd2Build.Versions.Game, message => Logger.LogInfo(message));
|
||||||
EnsureRecoveryHost();
|
EnsureRecoveryHost();
|
||||||
@@ -49,6 +52,8 @@ public sealed class Plugin : BaseUnityPlugin
|
|||||||
throw new MissingMethodException("IntroUI.Awake() was not found (client version mismatch)");
|
throw new MissingMethodException("IntroUI.Awake() was not found (client version mismatch)");
|
||||||
}
|
}
|
||||||
SendMaintenance = Game.Method<IntroUI>(ui => ui.SendMaintenanceInfo(false));
|
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);
|
SetIntroState = introUI.GetGameMethod("SetIntroState", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
EnterGame = introUI.GetGameMethod(
|
EnterGame = introUI.GetGameMethod(
|
||||||
"Enter",
|
"Enter",
|
||||||
@@ -72,7 +77,7 @@ public sealed class Plugin : BaseUnityPlugin
|
|||||||
MethodInfo exponentialBackOff = networkManager?.GetGameMethod(
|
MethodInfo exponentialBackOff = networkManager?.GetGameMethod(
|
||||||
"ExponetialBackOff",
|
"ExponetialBackOff",
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
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 ||
|
accessTokenGetter == null || clearPCLocalData == null || disposeWebRequest == null ||
|
||||||
clientNetworkError == null || exponentialBackOff == null || EnterGame == null)
|
clientNetworkError == null || exponentialBackOff == null || EnterGame == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ internal sealed class SecureGameRelay : IDisposable
|
|||||||
{
|
{
|
||||||
try { using (client) await Serve(client).ConfigureAwait(false); }
|
try { using (client) await Serve(client).ConfigureAwait(false); }
|
||||||
catch (Exception ex) when (ex is IOException || ex is SocketException || ex is OperationCanceledException || ex is InvalidDataException)
|
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); }
|
catch (Exception ex) { log?.LogWarning("Native game transport failed: " + ex.GetType().Name); }
|
||||||
finally { slots.Release(); }
|
finally { slots.Release(); }
|
||||||
});
|
});
|
||||||
@@ -108,6 +108,8 @@ internal sealed class SecureGameRelay : IDisposable
|
|||||||
throw new InvalidDataException("Invalid relay request");
|
throw new InvalidDataException("Invalid relay request");
|
||||||
if (!Uri.TryCreate(local, start[1], out Uri target) || !TryResolve(target, out Uri remote))
|
if (!Uri.TryCreate(local, start[1], out Uri target) || !TryResolve(target, out Uri remote))
|
||||||
throw new InvalidDataException("Invalid relay target");
|
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);
|
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
int length = 0;
|
int length = 0;
|
||||||
bool lengthSeen = false;
|
bool lengthSeen = false;
|
||||||
@@ -139,6 +141,9 @@ internal sealed class SecureGameRelay : IDisposable
|
|||||||
}
|
}
|
||||||
response = await PlatformControlHttp.Send(remote, start[0], body, null, 4, lifetime.Token,
|
response = await PlatformControlHttp.Send(remote, start[0], body, null, 4, lifetime.Token,
|
||||||
lifetime.Token, 64 * 1024 * 1024, headers).ConfigureAwait(false);
|
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");
|
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");
|
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)
|
foreach (KeyValuePair<string, string> item in response.Headers)
|
||||||
|
|||||||
@@ -225,6 +225,39 @@ internal static class SessionRecovery
|
|||||||
{
|
{
|
||||||
try
|
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))
|
if (ServerRoot == null || !IsConfiguredServerFailure(__0) || !IsTransportFailure(__0))
|
||||||
{
|
{
|
||||||
return true;
|
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;
|
return Volatile.Read(ref SessionRecoveryInProgress) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user