@@ -99,14 +99,25 @@ private async Task InitializeWebViewAsync()
9999 e . WebErrorStatus == CoreWebView2WebErrorStatus . ServerUnreachable ) )
100100 {
101101 Logger . Info ( "WebChatWindow: Gateway unreachable, showing friendly error" ) ;
102- WebView . Visibility = Visibility . Collapsed ;
103- ErrorPanel . Visibility = Visibility . Visible ;
104- ErrorText . Text = "Can't reach OpenClaw Gateway\n \n " +
102+ ShowErrorMessage ( "Can't reach OpenClaw Gateway\n \n " +
105103 $ "The gateway at { _gatewayUrl } is not responding.\n \n " +
106104 "To connect:\n " +
107105 "• Make sure your OpenClaw gateway is running\n " +
108106 "• If remote, connect via VPN to your home network\n " +
109- "• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server" ;
107+ "• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server" ) ;
108+ return ;
109+ }
110+
111+ if ( ! e . IsSuccess &&
112+ e . WebErrorStatus . ToString ( ) . Contains ( "Certificate" , StringComparison . OrdinalIgnoreCase ) )
113+ {
114+ Logger . Info ( "WebChatWindow: TLS certificate issue detected" ) ;
115+ ShowErrorMessage (
116+ "The gateway HTTPS certificate is not trusted.\n \n " +
117+ "To connect securely:\n " +
118+ "• Use an HTTPS gateway URL (for example: https://host.tailnet.ts.net)\n " +
119+ "• If self-signed, import the cert into Windows Trusted Root Certification Authorities\n " +
120+ "• Or use SSH tunnel to localhost and keep using localhost URLs" ) ;
110121 }
111122 } ;
112123 WebView . CoreWebView2 . NavigationCompleted += _navigationCompletedHandler ;
@@ -159,6 +170,59 @@ private async Task InitializeWebViewAsync()
159170
160171 // Set to a test URL to bypass gateway (e.g., "https://www.bing.com"), or null for normal operation
161172 private const string ? DEBUG_TEST_URL = null ;
173+
174+ private static bool IsLocalHost ( Uri uri )
175+ {
176+ return uri . IsLoopback || string . Equals ( uri . Host , "localhost" , StringComparison . OrdinalIgnoreCase ) ;
177+ }
178+
179+ private bool TryBuildChatUrl ( out string url , out string errorMessage )
180+ {
181+ url = string . Empty ;
182+ errorMessage = string . Empty ;
183+
184+ if ( ! GatewayUrlHelper . TryNormalizeWebSocketUrl ( _gatewayUrl , out var normalizedGatewayUrl ) ||
185+ ! Uri . TryCreate ( normalizedGatewayUrl , UriKind . Absolute , out var gatewayUri ) )
186+ {
187+ errorMessage = $ "Invalid gateway URL: { _gatewayUrl } ";
188+ return false ;
189+ }
190+
191+ var webScheme = gatewayUri . Scheme . Equals ( "wss" , StringComparison . OrdinalIgnoreCase )
192+ ? "https"
193+ : "http" ;
194+
195+ if ( webScheme == "http" && ! IsLocalHost ( gatewayUri ) )
196+ {
197+ errorMessage =
198+ "Web chat requires a secure context.\n \n " +
199+ "There is no safe bypass for remote plain HTTP: browsers and WebView enforce this.\n \n " +
200+ "Use one of these options:\n " +
201+ "• Use a trusted HTTPS/WSS endpoint (Let's Encrypt, Tailscale Serve, Caddy)\n " +
202+ "• If self-signed, import your gateway CA/cert into Windows Trusted Root (certmgr.msc)\n " +
203+ "• Or tunnel to localhost: ssh -N -L 18789:localhost:18789 <mac>" ;
204+ return false ;
205+ }
206+
207+ var builder = new UriBuilder ( gatewayUri )
208+ {
209+ Scheme = webScheme ,
210+ Port = gatewayUri . Port
211+ } ;
212+
213+ var baseUrl = builder . Uri . GetLeftPart ( UriPartial . Authority ) ;
214+ url = $ "{ baseUrl } ?token={ Uri . EscapeDataString ( _token ) } ";
215+ return true ;
216+ }
217+
218+ private void ShowErrorMessage ( string message )
219+ {
220+ LoadingRing . IsActive = false ;
221+ LoadingRing . Visibility = Visibility . Collapsed ;
222+ WebView . Visibility = Visibility . Collapsed ;
223+ ErrorPanel . Visibility = Visibility . Visible ;
224+ ErrorText . Text = message ;
225+ }
162226
163227 private void NavigateToChat ( )
164228 {
@@ -172,12 +236,15 @@ private void NavigateToChat()
172236 return ;
173237 }
174238
175- var baseUrl = _gatewayUrl
176- . Replace ( "ws://" , "http://" )
177- . Replace ( "wss://" , "https://" ) ;
178-
179- var url = $ "{ baseUrl } ?token={ Uri . EscapeDataString ( _token ) } ";
180- Logger . Info ( $ "WebChatWindow: Navigating to { baseUrl } (token hidden)") ;
239+ if ( ! TryBuildChatUrl ( out var url , out var errorMessage ) )
240+ {
241+ Logger . Warn ( $ "WebChatWindow: { errorMessage } ") ;
242+ ShowErrorMessage ( errorMessage ) ;
243+ return ;
244+ }
245+
246+ var safeBaseUrl = url . Split ( '?' ) [ 0 ] ;
247+ Logger . Info ( $ "WebChatWindow: Navigating to { safeBaseUrl } (token hidden)") ;
181248 WebView . CoreWebView2 . Navigate ( url ) ;
182249 }
183250
@@ -193,10 +260,12 @@ private void OnRefresh(object sender, RoutedEventArgs e)
193260
194261 private void OnPopout ( object sender , RoutedEventArgs e )
195262 {
196- var baseUrl = _gatewayUrl
197- . Replace ( "ws://" , "http://" )
198- . Replace ( "wss://" , "https://" ) ;
199- var url = $ "{ baseUrl } ?token={ Uri . EscapeDataString ( _token ) } ";
263+ if ( ! TryBuildChatUrl ( out var url , out var errorMessage ) )
264+ {
265+ Logger . Warn ( $ "WebChatWindow: { errorMessage } ") ;
266+ ShowErrorMessage ( errorMessage ) ;
267+ return ;
268+ }
200269
201270 try
202271 {
0 commit comments