11using System ;
2+ using System . Net ;
23using MCPForUnity . Editor . Constants ;
34using MCPForUnity . Editor . Models ;
45using MCPForUnity . Editor . Services ;
@@ -51,15 +52,15 @@ public static void SaveBaseUrl(string userValue)
5152 public static string GetLocalBaseUrl ( )
5253 {
5354 string stored = EditorPrefs . GetString ( LocalPrefKey , DefaultLocalBaseUrl ) ;
54- return NormalizeBaseUrl ( stored , DefaultLocalBaseUrl ) ;
55+ return NormalizeBaseUrl ( stored , DefaultLocalBaseUrl , remoteScope : false ) ;
5556 }
5657
5758 /// <summary>
5859 /// Saves a user-provided URL to the local HTTP pref.
5960 /// </summary>
6061 public static void SaveLocalBaseUrl ( string userValue )
6162 {
62- string normalized = NormalizeBaseUrl ( userValue , DefaultLocalBaseUrl ) ;
63+ string normalized = NormalizeBaseUrl ( userValue , DefaultLocalBaseUrl , remoteScope : false ) ;
6364 EditorPrefs . SetString ( LocalPrefKey , normalized ) ;
6465 }
6566
@@ -74,7 +75,7 @@ public static string GetRemoteBaseUrl()
7475 {
7576 return DefaultRemoteBaseUrl ;
7677 }
77- return NormalizeBaseUrl ( stored , DefaultRemoteBaseUrl ) ;
78+ return NormalizeBaseUrl ( stored , DefaultRemoteBaseUrl , remoteScope : true ) ;
7879 }
7980
8081 /// <summary>
@@ -87,7 +88,7 @@ public static void SaveRemoteBaseUrl(string userValue)
8788 EditorPrefs . SetString ( RemotePrefKey , DefaultRemoteBaseUrl ) ;
8889 return ;
8990 }
90- string normalized = NormalizeBaseUrl ( userValue , DefaultRemoteBaseUrl ) ;
91+ string normalized = NormalizeBaseUrl ( userValue , DefaultRemoteBaseUrl , remoteScope : true ) ;
9192 EditorPrefs . SetString ( RemotePrefKey , normalized ) ;
9293 }
9394
@@ -146,10 +147,169 @@ public static ConfiguredTransport GetCurrentServerTransport()
146147 return IsRemoteScope ( ) ? ConfiguredTransport . HttpRemote : ConfiguredTransport . Http ;
147148 }
148149
150+ /// <summary>
151+ /// Returns true when advanced settings allow binding HTTP Local to all interfaces
152+ /// (e.g. 0.0.0.0 / ::). Disabled by default.
153+ /// </summary>
154+ public static bool AllowLanHttpBind ( )
155+ {
156+ return EditorPrefs . GetBool ( EditorPrefKeys . AllowLanHttpBind , false ) ;
157+ }
158+
159+ /// <summary>
160+ /// Returns true when advanced settings allow insecure HTTP/WS for remote endpoints.
161+ /// Disabled by default.
162+ /// </summary>
163+ public static bool AllowInsecureRemoteHttp ( )
164+ {
165+ return EditorPrefs . GetBool ( EditorPrefKeys . AllowInsecureRemoteHttp , false ) ;
166+ }
167+
168+ /// <summary>
169+ /// Returns true if the host is loopback-only.
170+ /// </summary>
171+ public static bool IsLoopbackHost ( string host )
172+ {
173+ if ( string . IsNullOrWhiteSpace ( host ) )
174+ {
175+ return false ;
176+ }
177+
178+ string normalized = host . Trim ( ) . Trim ( '[' , ']' ) . ToLowerInvariant ( ) ;
179+ if ( normalized == "localhost" )
180+ {
181+ return true ;
182+ }
183+
184+ if ( IPAddress . TryParse ( normalized , out IPAddress parsedIp ) )
185+ {
186+ return IPAddress . IsLoopback ( parsedIp ) ;
187+ }
188+
189+ return false ;
190+ }
191+
192+ /// <summary>
193+ /// Returns true if the host is a bind-all-interfaces address.
194+ /// </summary>
195+ public static bool IsBindAllInterfacesHost ( string host )
196+ {
197+ if ( string . IsNullOrWhiteSpace ( host ) )
198+ {
199+ return false ;
200+ }
201+
202+ string normalized = host . Trim ( ) . Trim ( '[' , ']' ) . ToLowerInvariant ( ) ;
203+ if ( IPAddress . TryParse ( normalized , out IPAddress parsedIp ) )
204+ {
205+ return parsedIp . Equals ( IPAddress . Any ) || parsedIp . Equals ( IPAddress . IPv6Any ) ;
206+ }
207+
208+ return false ;
209+ }
210+
211+ /// <summary>
212+ /// Returns true when the URL host is acceptable for HTTP Local launch.
213+ /// Loopback is always allowed. Bind-all interfaces requires explicit opt-in.
214+ /// </summary>
215+ public static bool IsHttpLocalUrlAllowedForLaunch ( string url , out string error )
216+ {
217+ error = null ;
218+ if ( string . IsNullOrWhiteSpace ( url ) )
219+ {
220+ error = "HTTP Local requires a loopback URL (localhost/127.0.0.1/::1)." ;
221+ return false ;
222+ }
223+
224+ if ( ! Uri . TryCreate ( url , UriKind . Absolute , out var uri ) )
225+ {
226+ error = $ "Invalid URL: { url } ";
227+ return false ;
228+ }
229+
230+ string host = uri . Host ;
231+ if ( IsLoopbackHost ( host ) )
232+ {
233+ return true ;
234+ }
235+
236+ if ( IsBindAllInterfacesHost ( host ) )
237+ {
238+ if ( AllowLanHttpBind ( ) )
239+ {
240+ return true ;
241+ }
242+
243+ error = "Binding to all interfaces (0.0.0.0/::) is disabled by default. " +
244+ "Enable \" Allow LAN bind for HTTP Local\" in Advanced Settings to opt in." ;
245+ return false ;
246+ }
247+
248+ error = "HTTP Local requires a loopback URL (localhost/127.0.0.1/::1)." ;
249+ return false ;
250+ }
251+
252+ /// <summary>
253+ /// Returns true when remote URL is allowed by current security policy.
254+ /// HTTPS is required by default; HTTP needs explicit opt-in.
255+ /// </summary>
256+ public static bool IsRemoteUrlAllowed ( string remoteBaseUrl , out string error )
257+ {
258+ error = null ;
259+ if ( string . IsNullOrWhiteSpace ( remoteBaseUrl ) )
260+ {
261+ error = "HTTP Remote requires a configured URL." ;
262+ return false ;
263+ }
264+
265+ if ( ! Uri . TryCreate ( remoteBaseUrl , UriKind . Absolute , out var uri ) )
266+ {
267+ error = $ "Invalid HTTP Remote URL: { remoteBaseUrl } ";
268+ return false ;
269+ }
270+
271+ if ( uri . Scheme . Equals ( "https" , StringComparison . OrdinalIgnoreCase ) )
272+ {
273+ return true ;
274+ }
275+
276+ if ( uri . Scheme . Equals ( "http" , StringComparison . OrdinalIgnoreCase ) )
277+ {
278+ if ( AllowInsecureRemoteHttp ( ) )
279+ {
280+ return true ;
281+ }
282+
283+ error = "HTTP Remote requires HTTPS by default. Enable \" Allow insecure HTTP for HTTP Remote\" in Advanced Settings to opt in." ;
284+ return false ;
285+ }
286+
287+ error = $ "Unsupported URL scheme '{ uri . Scheme } '. Use https:// (or http:// only with explicit insecure opt-in).";
288+ return false ;
289+ }
290+
291+ /// <summary>
292+ /// Returns true when the currently configured remote URL satisfies security policy.
293+ /// </summary>
294+ public static bool IsCurrentRemoteUrlAllowed ( out string error )
295+ {
296+ return IsRemoteUrlAllowed ( GetRemoteBaseUrl ( ) , out error ) ;
297+ }
298+
299+ /// <summary>
300+ /// Human-readable host requirement for HTTP Local based on current security settings.
301+ /// </summary>
302+ public static string GetHttpLocalHostRequirementText ( )
303+ {
304+ return AllowLanHttpBind ( )
305+ ? "localhost/127.0.0.1/::1/0.0.0.0/::"
306+ : "localhost/127.0.0.1/::1" ;
307+ }
308+
149309 /// <summary>
150310 /// Normalizes a URL so that we consistently store just the base (no trailing slash/path).
151311 /// </summary>
152- private static string NormalizeBaseUrl ( string value , string defaultUrl )
312+ private static string NormalizeBaseUrl ( string value , string defaultUrl , bool remoteScope )
153313 {
154314 if ( string . IsNullOrWhiteSpace ( value ) )
155315 {
@@ -158,10 +318,13 @@ private static string NormalizeBaseUrl(string value, string defaultUrl)
158318
159319 string trimmed = value . Trim ( ) ;
160320
161- // Ensure scheme exists; default to http:// if user omitted it.
321+ // Ensure scheme exists.
322+ // For HTTP Remote, default to https:// to avoid accidental plaintext transport.
323+ // For HTTP Local, default to http:// for zero-friction local setup.
162324 if ( ! trimmed . Contains ( "://" ) )
163325 {
164- trimmed = $ "http://{ trimmed } ";
326+ string defaultScheme = remoteScope ? "https" : "http" ;
327+ trimmed = $ "{ defaultScheme } ://{ trimmed } ";
165328 }
166329
167330 // Remove trailing slash segments.
0 commit comments