diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs index 4307d4421..d1f8d77d5 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Uwb/UwbWebView.cs @@ -1,4 +1,4 @@ -#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) +#if UWB_WEBVIEW && !IMMUTABLE_CUSTOM_BROWSER && (UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN)) using System; using System.IO; @@ -32,6 +32,7 @@ public class UwbWebView : MonoBehaviour, IWebBrowserClient #endif private WebBrowserClient? webBrowserClient; + private GameBridgeServer? gameBridgeServer; public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, Func redactionHandler) { @@ -66,8 +67,9 @@ public async UniTask Init(int engineStartupTimeoutMs, bool redactTokensInLogs, F var browserEngineMainDir = WebBrowserUtils.GetAdditionFilesDirectory(); browserClient.CachePath = new FileInfo(Path.Combine(browserEngineMainDir, "ImmutableSDK/UWBCache")); - // Game bridge path - browserClient.initialUrl = GameBridge.GetFilePath(); + // Start local HTTP server to serve index.html + gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath()); + browserClient.initialUrl = gameBridgeServer.Start(); // Set up engine from standard UWB configuration asset var engineConfigAsset = Resources.Load("Cef Engine Configuration"); @@ -145,6 +147,8 @@ public void Dispose() if (webBrowserClient?.HasDisposed == true) return; webBrowserClient?.Dispose(); + gameBridgeServer?.Dispose(); + gameBridgeServer = null; } #endif } diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs index 3e948812c..3fd11fab6 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridge.cs @@ -41,5 +41,15 @@ public static string GetFilePath() filePath = filePath.Replace(" ", "%20"); return filePath; } + + /// + /// Gets the file system path to index.html (without file:// scheme or URL encoding). + /// + public static string GetFileSystemPath() + { + return GetFilePath() + .Replace(SCHEME_FILE, "") + .Replace("%20", " "); + } } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs new file mode 100644 index 000000000..e0bb53c6b --- /dev/null +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs @@ -0,0 +1,167 @@ +#if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using UnityEngine; + +namespace Immutable.Browser.Core +{ + /// + /// Local HTTP server for index.html to provide a proper origin instead of null from file:// URLs. + /// + public class GameBridgeServer : IDisposable + { + private const string TAG = "[Game Bridge Server]"; + + // Fixed port to maintain consistent origin for localStorage/IndexedDB persistence + private const int PORT = 51990; + private static readonly string URL = "http://localhost:" + PORT + "/"; + + private HttpListener? _listener; + private Thread? _listenerThread; + private byte[]? _indexHtmlContent; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private bool _disposed; + + /// + /// Creates a new GameBridgeServer instance. + /// + /// The file system path to the index.html file. + public GameBridgeServer(string indexHtmlPath) + { + if (string.IsNullOrEmpty(indexHtmlPath)) + throw new ArgumentNullException(nameof(indexHtmlPath)); + + if (!File.Exists(indexHtmlPath)) + throw new FileNotFoundException($"{TAG} index.html not found: {indexHtmlPath}"); + + _indexHtmlContent = File.ReadAllBytes(indexHtmlPath); + Debug.Log($"{TAG} Loaded index.html ({_indexHtmlContent.Length} bytes)"); + } + + /// + /// Starts the game bridge server. + /// + /// The URL to the index.html file. + public string Start() + { + if (_disposed) + throw new ObjectDisposedException(nameof(GameBridgeServer)); + + if (_listener?.IsListening == true) + return URL; + + EnsurePortAvailable(); + + _listener = new HttpListener(); + _listener.Prefixes.Add(URL); + _listener.Start(); + + Debug.Log($"{TAG} Started on {URL}"); + + _listenerThread = new Thread(ListenerLoop) + { + Name = "GameBridgeServer", + IsBackground = true + }; + _listenerThread.Start(); + + return URL; + } + + private void ListenerLoop() + { + while (!_cts.Token.IsCancellationRequested && _listener?.IsListening == true) + { + try + { + var context = _listener.GetContext(); + HandleRequest(context); + } + catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + if (!_cts.Token.IsCancellationRequested) + Debug.LogError($"{TAG} Error: {ex.Message}"); + } + } + } + + private void HandleRequest(HttpListenerContext context) + { + var response = context.Response; + try + { + response.StatusCode = 200; + response.ContentType = "text/html; charset=utf-8"; + response.ContentLength64 = _indexHtmlContent!.Length; + response.OutputStream.Write(_indexHtmlContent, 0, _indexHtmlContent.Length); + } + catch (Exception ex) + { + Debug.LogError($"{TAG} Error handling request: {ex.Message}"); + } + finally + { + try { response.Close(); } catch { } + } + } + + private void EnsurePortAvailable() + { + if (!IsPortAvailable(PORT)) + { + throw new InvalidOperationException( + $"{TAG} Port {PORT} is already in use. " + + "Please close any application using this port to ensure localStorage/IndexedDB data persists correctly."); + } + } + + private bool IsPortAvailable(int port) + { + try + { + var listener = new TcpListener(IPAddress.Loopback, port); + listener.Start(); + listener.Stop(); + return true; + } + catch + { + return false; + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _cts.Cancel(); + try + { + _listener?.Stop(); + _listener?.Close(); + } + catch { } + + _listenerThread?.Join(TimeSpan.FromSeconds(1)); + _cts.Dispose(); + _indexHtmlContent = null; + + Debug.Log($"{TAG} Stopped"); + } + } +} + +#endif diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta new file mode 100644 index 000000000..eaf5251c1 --- /dev/null +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/GameBridgeServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab2317c6b7a6cea4391c77dae3a5deb7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs index efa949343..5501b0572 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/WindowsWebBrowserClientAdapter.cs @@ -1,8 +1,9 @@ #if UNITY_STANDALONE_WIN || (UNITY_ANDROID && UNITY_EDITOR_WIN) || (UNITY_IPHONE && UNITY_EDITOR_WIN) -using System.IO; +using System.Net.Sockets; using UnityEngine; using Immutable.Browser.Core; +using Immutable.Passport; using Immutable.Passport.Core.Logging; using Cysharp.Threading.Tasks; @@ -13,6 +14,7 @@ public class WindowsWebBrowserClientAdapter : IWebBrowserClient public event OnUnityPostMessageDelegate OnUnityPostMessage; private readonly IWindowsWebBrowserClient webBrowserClient; + private GameBridgeServer? gameBridgeServer; public WindowsWebBrowserClientAdapter(IWindowsWebBrowserClient windowsWebBrowserClient) { @@ -33,8 +35,11 @@ public async UniTask Init() // Initialise the web browser client asynchronously await webBrowserClient.Init(); + // Start local HTTP server to serve index.html + gameBridgeServer = new GameBridgeServer(GameBridge.GetFileSystemPath()); + // Load the game bridge file into the web browser client - webBrowserClient.LoadUrl(GameBridge.GetFilePath()); + webBrowserClient.LoadUrl(gameBridgeServer.Start()); // Get the JavaScript API call for posting messages from the web page to the Unity application string postMessageApiCall = webBrowserClient.GetPostMessageApiCall(); @@ -59,6 +64,8 @@ public void LaunchAuthURL(string url, string? redirectUri) public void Dispose() { webBrowserClient.Dispose(); + gameBridgeServer?.Dispose(); + gameBridgeServer = null; } } }