diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/NFTShapes/LoadNFTTypeSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/NFTShapes/LoadNFTTypeSystem.cs index ccf31a7bc15..34b2ba73f4d 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/NFTShapes/LoadNFTTypeSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/NFTShapes/LoadNFTTypeSystem.cs @@ -38,7 +38,11 @@ protected override async UniTask> FlowInt StreamableLoadingState state, IPartitionComponent partition, CancellationToken ct) { string imageUrl = await ImageUrlAsync(intention.CommonArguments, ct); - string convertUrl = ktxEnabled ? string.Format(urlsSource.Url(DecentralandUrl.MediaConverter), Uri.EscapeDataString(imageUrl)) : imageUrl; + + // Same guard as GetTextureWebRequest: non-publicly-routable URLs must not reach the hosted converter. + string convertUrl = ktxEnabled && !DCL.WebRequests.WebRequestUtils.IsNonPubliclyRoutable(imageUrl) + ? string.Format(urlsSource.Url(DecentralandUrl.MediaConverter), Uri.EscapeDataString(imageUrl)) + : imageUrl; var contentInfo = await WebContentInfo.FetchAsync(convertUrl, ct); if (!ktxEnabled && contentInfo is { Type: WebContentInfo.ContentType.Image, SizeInBytes: > MAX_PREVIEW_SIZE }) diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/WebSocketArchipelagoLiveConnection.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/WebSocketArchipelagoLiveConnection.cs index f6151a2fde7..b69dd86984a 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/WebSocketArchipelagoLiveConnection.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/WebSocketArchipelagoLiveConnection.cs @@ -20,6 +20,14 @@ public class WebSocketArchipelagoLiveConnection : IArchipelagoLiveConnection { private const int BUFFER_SIZE = 1024 * 1024; //1MB + /// + /// Budget for the graceful close handshake before hard-dropping the socket. The server ack + /// is only worth waiting for briefly: the receive loop that would read it is already + /// cancelled when a room stops, so an unbounded CloseAsync hung the teleport pipeline + /// into its 10s timeout. + /// + private static readonly TimeSpan CLOSE_HANDSHAKE_BUDGET = TimeSpan.FromSeconds(1); + private readonly IMemoryPool memoryPool; private Current? current; @@ -48,8 +56,22 @@ public async UniTask DisconnectAsync(CancellationToken token) { try { - TryUpdateWebSocket(); - await current!.Value.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, token); + if (current?.WebSocket.State is WebSocketState.Open or WebSocketState.Connecting) + { + using var closeCts = CancellationTokenSource.CreateLinkedTokenSource(token); + closeCts.CancelAfter(CLOSE_HANDSHAKE_BUDGET); + + try { await current!.Value.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, closeCts.Token); } + catch (OperationCanceledException) when (token.IsCancellationRequested) { throw; } + catch (Exception) + { + // The handshake exceeded its budget or the socket errored out; hard-drop below. + } + } + + // Install a fresh socket so a subsequent ConnectAsync can never race the old instance. + current?.Dispose(); + current = Current.New(); return Result.SuccessResult(); } catch (Exception e) { return Result.ErrorResult($"Cannot disconnect: {e}"); } diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/Rooms/ArchipelagoIslandRoom.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/Rooms/ArchipelagoIslandRoom.cs index c9fd01336af..a14cb9a1274 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/Rooms/ArchipelagoIslandRoom.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/Rooms/ArchipelagoIslandRoom.cs @@ -56,6 +56,20 @@ ICurrentAdapterAddress currentAdapterAddress this.currentAdapterAddress = currentAdapterAddress; } + public override async UniTask StopAsync() + { + await base.StopAsync(); + + // Close the sign-flow ws on stop. Otherwise the next StartAsync reuses the + // still-open socket (EnsureConnectionAsync sees IsConnected and skips the + // handshake), the server keeps the old peer+island, never re-sends + // IslandChanged — and the restarted room waits for a connection string that + // never arrives (observed as 3x10s RestartRoom timeouts on every + // world->genesis realm change). + try { await signFlow.DisconnectAsync(CancellationToken.None); } + catch (Exception e) { ReportHub.LogWarning(ReportCategory.COMMS_SCENE_HANDLER, $"ArchipelagoIslandRoom stop: disconnect failed: {e.Message}"); } + } + protected override async UniTask PrewarmAsync(CancellationToken token) { // Reset the per-session state: the room instance is reused across StopAsync/StartAsync (teleport, logout) diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/GateKeeper/Rooms/IGateKeeperSceneRoom.cs b/Explorer/Assets/DCL/Multiplayer/Connections/GateKeeper/Rooms/IGateKeeperSceneRoom.cs index ca343ed0098..662980feeeb 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/GateKeeper/Rooms/IGateKeeperSceneRoom.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/GateKeeper/Rooms/IGateKeeperSceneRoom.cs @@ -35,9 +35,9 @@ public interface IGateKeeperSceneRoom : IActivatableConnectiveRoom, ISceneRoomSt class Fake : Null, IGateKeeperSceneRoom { - public event Action? CurrentSceneRoomConnected; - public event Action? CurrentSceneRoomDisconnected; - public event Action? CurrentSceneRoomForbiddenAccess; + public event Action? CurrentSceneRoomConnected { add { } remove { } } + public event Action? CurrentSceneRoomDisconnected { add { } remove { } } + public event Action? CurrentSceneRoomForbiddenAccess { add { } remove { } } public MetaData? ConnectedScene { get; } = new MetaData("Fake", Vector2Int.zero, new MetaData.Input()); public bool Activated => true; diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/NullRoom.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/NullRoom.cs index 7f9498793ef..5e66e6eba5a 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/NullRoom.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/NullRoom.cs @@ -34,21 +34,21 @@ public class NullRoom : IRoom public IAudioStreams AudioStreams => NullAudioStreams.INSTANCE; public ILocalTracks LocalTracks => NullLocalTracks.INSTANCE; - public event LocalPublishDelegate? LocalTrackPublished; - public event LocalPublishDelegate? LocalTrackUnpublished; - public event PublishDelegate? TrackPublished; - public event PublishDelegate? TrackUnpublished; - public event SubscribeDelegate? TrackSubscribed; - public event SubscribeDelegate? TrackUnsubscribed; - public event MuteDelegate? TrackMuted; - public event MuteDelegate? TrackUnmuted; + public event LocalPublishDelegate? LocalTrackPublished { add { } remove { } } + public event LocalPublishDelegate? LocalTrackUnpublished { add { } remove { } } + public event PublishDelegate? TrackPublished { add { } remove { } } + public event PublishDelegate? TrackUnpublished { add { } remove { } } + public event SubscribeDelegate? TrackSubscribed { add { } remove { } } + public event SubscribeDelegate? TrackUnsubscribed { add { } remove { } } + public event MuteDelegate? TrackMuted { add { } remove { } } + public event MuteDelegate? TrackUnmuted { add { } remove { } } #endif - public event ConnectionQualityChangeDelegate? ConnectionQualityChanged; - public event ConnectionStateChangeDelegate? ConnectionStateChanged; - public event ConnectionDelegate? ConnectionUpdated; - public event Room.MetaDelegate? RoomMetadataChanged; - public event Room.SidDelegate? RoomSidChanged; + public event ConnectionQualityChangeDelegate? ConnectionQualityChanged { add { } remove { } } + public event ConnectionStateChangeDelegate? ConnectionStateChanged { add { } remove { } } + public event ConnectionDelegate? ConnectionUpdated { add { } remove { } } + public event Room.MetaDelegate? RoomMetadataChanged { add { } remove { } } + public event Room.SidDelegate? RoomSidChanged { add { } remove { } } public void UpdateLocalMetadata(string metadata) { diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullActiveSpeakers.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullActiveSpeakers.cs index 1349c939e77..8718df784cf 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullActiveSpeakers.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullActiveSpeakers.cs @@ -11,7 +11,7 @@ public class NullActiveSpeakers : IActiveSpeakers public int Count => 0; - public event Action? Updated; + public event Action? Updated { add { } remove { } } public IEnumerator GetEnumerator() { diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullDataPipe.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullDataPipe.cs index 84559839676..fa2f3106536 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullDataPipe.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullDataPipe.cs @@ -10,7 +10,7 @@ public class NullDataPipe : IDataPipe { public static readonly NullDataPipe INSTANCE = new (); - public event ReceivedDataDelegate? DataReceived; + public event ReceivedDataDelegate? DataReceived { add { } remove { } } public void PublishData(Span data, string topic, IReadOnlyCollection destinationSids, LKDataPacketKind kind = LKDataPacketKind.KindLossy) { diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullParticipantsHub.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullParticipantsHub.cs index 8eeb94113da..8bc7ea7b39b 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullParticipantsHub.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Rooms/Nulls/NullParticipantsHub.cs @@ -12,7 +12,7 @@ public class NullParticipantsHub : IParticipantsHub public static readonly LKParticipant NULL_PARTICIPANT = new (); public static readonly WeakReference WEAK_NULL_PARTICIPANT = new (NULL_PARTICIPANT); - public event ParticipantDelegate? UpdatesFromParticipant; + public event ParticipantDelegate? UpdatesFromParticipant { add { } remove { } } public LKParticipant LocalParticipant() => NULL_PARTICIPANT; diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Systems/RoomIndicator.prefab b/Explorer/Assets/DCL/Multiplayer/Connections/Systems/RoomIndicator.prefab index b3cc380b3f9..5cc9c2b145e 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Systems/RoomIndicator.prefab +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Systems/RoomIndicator.prefab @@ -9,7 +9,6 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 6615945845340605545} - - component: {fileID: 6145739770501069584} m_Layer: 5 m_Name: RoomIndicator m_TagString: Untagged @@ -39,33 +38,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0.45, y: 0.3} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &6145739770501069584 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2074114889938352466} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d7d34f10598c4f638b94710f3b0ff918, type: 3} - m_Name: - m_EditorClassIdentifier: - colors: - - source: 0 - indicator: N/A - color: {r: 0.41509432, g: 0.41509432, b: 0.41509432, a: 1} - - source: 1 - indicator: G - color: {r: 0.07143695, g: 0.26501098, b: 0.5283019, a: 1} - - source: 2 - indicator: I - color: {r: 0.07058824, g: 0.5294118, b: 0.22292237, a: 1} - - source: 3 - indicator: I+G - color: {r: 0.55345905, g: 0.06787699, b: 0.48883247, a: 1} - background: {fileID: 2968721729092005038} - text: {fileID: 1326886004246566054} --- !u!1 &2433736487283086053 GameObject: m_ObjectHideFlags: 0 @@ -123,6 +95,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -144,9 +118,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 10 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!114 &1326886004246566054 MonoBehaviour: @@ -206,6 +182,7 @@ MonoBehaviour: m_VerticalAlignment: 512 m_textAlignment: 65535 m_characterSpacing: 0 + m_characterHorizontalScale: 1 m_wordSpacing: 0 m_lineSpacing: 0 m_lineSpacingMax: 0 @@ -280,6 +257,7 @@ RectTransform: m_Pivot: {x: 0.5, y: 0.5} --- !u!212 &544308281080233652 SpriteRenderer: + serializedVersion: 2 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -298,6 +276,8 @@ SpriteRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -319,9 +299,11 @@ SpriteRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 1 + m_MaskInteraction: 0 m_Sprite: {fileID: 21300000, guid: 37894a231569347e8b89612f29611b85, type: 3} m_Color: {r: 0.13836467, g: 0.13836467, b: 0.13836467, a: 1} m_FlipX: 0 @@ -331,7 +313,6 @@ SpriteRenderer: m_AdaptiveModeThreshold: 0.5 m_SpriteTileMode: 0 m_WasSpriteAssigned: 1 - m_MaskInteraction: 0 m_SpriteSortPoint: 0 --- !u!1 &3907013848479446928 GameObject: @@ -371,6 +352,7 @@ RectTransform: m_Pivot: {x: 0.5, y: 0.5} --- !u!212 &2968721729092005038 SpriteRenderer: + serializedVersion: 2 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -389,6 +371,8 @@ SpriteRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -410,9 +394,11 @@ SpriteRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_Sprite: {fileID: 21300000, guid: 12dd1efc4e826764f9b02be515a9a033, type: 3} m_Color: {r: 0.5529412, g: 0.06666667, b: 0.49019608, a: 1} m_FlipX: 0 @@ -422,5 +408,4 @@ SpriteRenderer: m_AdaptiveModeThreshold: 0.5 m_SpriteTileMode: 0 m_WasSpriteAssigned: 1 - m_MaskInteraction: 0 m_SpriteSortPoint: 0 diff --git a/Explorer/Assets/DCL/Social/WebSocketRpcTransport.cs b/Explorer/Assets/DCL/Social/WebSocketRpcTransport.cs index 4eaf16d15c4..3a85814634f 100644 --- a/Explorer/Assets/DCL/Social/WebSocketRpcTransport.cs +++ b/Explorer/Assets/DCL/Social/WebSocketRpcTransport.cs @@ -94,6 +94,7 @@ async UniTaskVoid ListenAndProcessIncomingDataAsync(CancellationToken ct) } } catch (OperationCanceledException) { break; } + catch (ObjectDisposedException) { break; } catch (WebSocketException e) { OnErrorEvent?.Invoke(e); @@ -119,6 +120,12 @@ public virtual async UniTask SendMessageAsync(byte[] data, CancellationToken ct) { OnErrorEvent?.Invoke(e); } + // The socket can be disposed mid-send during teardown/reconnect (isDisposed was false at + // entry); surface it like a send failure so callers don't believe the message was delivered. + catch (ObjectDisposedException e) + { + OnErrorEvent?.Invoke(e); + } } public virtual async UniTask SendMessageAsync(string data, CancellationToken ct) @@ -130,6 +137,10 @@ public virtual async UniTask SendMessageAsync(string data, CancellationToken ct) { OnErrorEvent?.Invoke(e); } + catch (ObjectDisposedException e) + { + OnErrorEvent?.Invoke(e); + } } public void Close() => @@ -141,7 +152,8 @@ public async UniTask CloseAsync(CancellationToken ct) if (State is WebSocketState.Open or WebSocketState.CloseReceived) { - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", ct); + try { await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", ct); } + catch (ObjectDisposedException) { return; } OnCloseEvent?.Invoke(); } } diff --git a/Explorer/Assets/DCL/WebRequests/Dumper/EnvelopeJsonConverter.cs b/Explorer/Assets/DCL/WebRequests/Dumper/EnvelopeJsonConverter.cs index ca13121187b..4a204dc89fc 100644 --- a/Explorer/Assets/DCL/WebRequests/Dumper/EnvelopeJsonConverter.cs +++ b/Explorer/Assets/DCL/WebRequests/Dumper/EnvelopeJsonConverter.cs @@ -143,8 +143,8 @@ public override void WriteJson(JsonWriter writer, WebRequestDump.Envelope? value } } - if (!commonArguments.HasValue || argsType == null) - throw new JsonSerializationException("Required properties 'commonArguments' or 'argsType' are missing"); + if (!commonArguments.HasValue || argsType == null || requestType == null || args == null) + throw new JsonSerializationException("Required properties 'commonArguments', 'requestType', 'argsType', or 'args' are missing"); var envelope = new WebRequestDump.Envelope(requestType, commonArguments.Value, argsType, args, headersInfo, startTime, effectiveUrl); envelope.Conclude(statusKind, endTime); diff --git a/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs b/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs index 0b483d8a223..be56048be5d 100644 --- a/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs +++ b/Explorer/Assets/DCL/WebRequests/GenericDownloadHandlerUtils.cs @@ -322,6 +322,10 @@ public OverwriteFromJsonAsyncOp(T target, WRJsonParser jsonParser, WRThreadFlags using var textReader = new StreamReader(stream, Encoding.UTF8); using var jsonReader = new JsonTextReader(textReader); + + if (Target == null) + throw new InvalidOperationException($"{nameof(OverwriteFromJsonAsyncOp)} requires a non-null {nameof(Target)} to populate"); + serializer.Populate(jsonReader, Target); } } diff --git a/Explorer/Assets/DCL/WebRequests/Texture/GetTextureWebRequest.cs b/Explorer/Assets/DCL/WebRequests/Texture/GetTextureWebRequest.cs index a188bf23db7..3e9963f1469 100644 --- a/Explorer/Assets/DCL/WebRequests/Texture/GetTextureWebRequest.cs +++ b/Explorer/Assets/DCL/WebRequests/Texture/GetTextureWebRequest.cs @@ -37,7 +37,9 @@ public static CreateTextureOp CreateTexture(TextureWrapMode wrapMode, FilterMode internal static GetTextureWebRequest Initialize(string url, GetTextureArguments textureArguments, IDecentralandUrlsSource urlsSource, bool ktxEnabled) { - bool useKtx = textureArguments.UseKtx && ktxEnabled && !WebRequestUtils.IsLocalhost(url); + // Never hand a non-publicly-routable content URL to the hosted converter: its workers + // cannot reach the host, so the job hangs until TCP timeout and clogs the converter queue. + bool useKtx = textureArguments.UseKtx && ktxEnabled && !WebRequestUtils.IsNonPubliclyRoutable(url); string requestUrl = useKtx ? string.Format(urlsSource.Url(DecentralandUrl.MediaConverter), Uri.EscapeDataString(url)) : url; UnityWebRequest webRequest = UnityWebRequest.Get(requestUrl); diff --git a/Explorer/Assets/DCL/WebRequests/WebRequestUtils.cs b/Explorer/Assets/DCL/WebRequests/WebRequestUtils.cs index db79b550bc5..dcd98024c1e 100644 --- a/Explorer/Assets/DCL/WebRequests/WebRequestUtils.cs +++ b/Explorer/Assets/DCL/WebRequests/WebRequestUtils.cs @@ -176,6 +176,50 @@ public static bool IsLocalhost(string url) => || url.StartsWith("http://[::1]", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://[::1]", StringComparison.OrdinalIgnoreCase); + /// + /// True when the URL's host is syntactically recognizable as non-publicly-routable and + /// therefore unreachable from hosted Decentraland services (the media converter, etc.): + /// loopback, RFC1918, CGNAT (100.64/10), link-local, unique-local IPv6 and the + /// .local/.internal/.lan suffixes. It does NOT resolve DNS, so a public hostname that + /// resolves to a private address still passes. + /// + public static bool IsNonPubliclyRoutable(string url) + { + if (IsLocalhost(url)) return true; + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri)) return false; + + string host = uri.Host; + + if (uri.IsLoopback + || host.EndsWith(".local", StringComparison.OrdinalIgnoreCase) + || host.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) + || host.EndsWith(".lan", StringComparison.OrdinalIgnoreCase)) + return true; + + if (System.Net.IPAddress.TryParse(host.Trim('[', ']'), out System.Net.IPAddress? ip)) + { + if (System.Net.IPAddress.IsLoopback(ip)) return true; + + Span b = stackalloc byte[16]; + + if (!ip.TryWriteBytes(b, out int written)) return false; + + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork && written == 4) + { + return b[0] == 10 + || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) + || (b[0] == 192 && b[1] == 168) + || (b[0] == 100 && b[1] >= 64 && b[1] <= 127) + || (b[0] == 169 && b[1] == 254); + } + + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + return ip.IsIPv6LinkLocal || ip.IsIPv6SiteLocal || (b[0] & 0xFE) == 0xFC; + } + + return false; + } + /// /// Does nothing with the web request ///