11using CommunicationData . URLHelpers ;
22using DCL . Diagnostics ;
33using DCL . Multiplayer . Connections . DecentralandUrls ;
4+ using DCL . Web3 . Chains ;
45using DCL . Web3 . Identities ;
56using NSubstitute ;
67using NUnit . Framework ;
1112
1213namespace DCL . WebRequests . Tests
1314{
14- // Transport-security policy invariants: cleartext http is loopback-only; http to any
15- // other host is upgraded to https on the wire URL of the built request (after per-request
16- // URL composition, so URLs embedded in it as data survive); the player-level
15+ // Transport-security policy invariants: cleartext http is loopback-only where the policy
16+ // binds — at infra URL-resolution sites (media resolution, sidecar realm root) and on the
17+ // wire URL of signed requests (the identity auth chain never travels over forbidden
18+ // cleartext). Unsigned wire URLs pass through the envelope unchanged: their scheme is a
19+ // module-level decision (local-scene-development fetch permits cleartext to any host).
20+ // The redirect guard blocks only mid-flight downgrades, and the player-level
1721 // insecureHttpOption stays AlwaysAllowed so the client-side policy is the single
1822 // enforcement point.
1923 public class InsecureSchemePolicyShould
2024 {
2125 private const string MEDIA_CONVERTER_TEMPLATE = "https://metamorph-api.decentraland.org/convert?url={0}" ;
2226
27+ [ TestCase ( "http://192.168.1.50:8000/api" ) ]
28+ [ TestCase ( "http://peer.decentraland.org/x" ) ]
29+ public void PassNonLoopbackHttpThroughUnchangedWhenUnsigned ( string url )
30+ {
31+ // A local-scene-development scene fetch is an unsigned request whose cleartext
32+ // scheme is the fetch module's own vetted decision; the envelope must not rewrite it
33+ Assert . That ( UrlAfterEnvelopeInitialization ( url ) , Is . EqualTo ( UnityCanonicalUrl ( url ) ) ) ;
34+ }
35+
2336 [ Test ]
24- public void UpgradeNonLoopbackHttpToHttps ( )
37+ public void UpgradeNonLoopbackHttpToHttpsWhenSigned ( )
2538 {
2639 Assert . That (
27- UrlAfterEnvelopeInitialization ( "http://peer.decentraland.org/x" ) ,
40+ UrlAfterEnvelopeInitialization ( "http://peer.decentraland.org/x" , new WebRequestSignInfo ( string . Empty ) ) ,
2841 Is . EqualTo ( UnityCanonicalUrl ( "https://peer.decentraland.org/x" ) ) ) ;
2942 }
3043
44+ [ TestCase ( "http://127.0.0.1:8000/content/contents/bafkreib" ) ]
45+ [ TestCase ( "http://localhost:8001/x" ) ]
46+ public void PassLoopbackHttpThroughUnchangedWhenSigned ( string url )
47+ {
48+ Assert . That (
49+ UrlAfterEnvelopeInitialization ( url , new WebRequestSignInfo ( string . Empty ) ) ,
50+ Is . EqualTo ( UnityCanonicalUrl ( url ) ) ) ;
51+ }
52+
3153 [ TestCase ( "http://127.0.0.1:8000/content/contents/bafkreib" ) ]
3254 [ TestCase ( "http://127.0.0.5:8000/x" ) ]
3355 [ TestCase ( "http://localhost:8001/x" ) ]
@@ -45,11 +67,13 @@ public void PassNonHttpSchemesThroughUnchanged(string url)
4567 }
4668
4769 [ Test ]
48- public void UpgradeNonConvertedTextureUrlAtTheWire ( )
70+ public void PassNonConvertedTextureUrlThroughUnchangedAtTheWire ( )
4971 {
72+ const string HTTP_URL = "http://textures.example.com/a.png" ;
73+
5074 Assert . That (
51- TextureUrlAfterEnvelopeInitialization ( "http://textures.example.com/a.png" , ktxEnabled : false ) ,
52- Is . EqualTo ( UnityCanonicalUrl ( "https://textures.example.com/a.png" ) ) ) ;
75+ TextureUrlAfterEnvelopeInitialization ( HTTP_URL , ktxEnabled : false ) ,
76+ Is . EqualTo ( UnityCanonicalUrl ( HTTP_URL ) ) ) ;
5377 }
5478
5579 [ Test ]
@@ -93,11 +117,22 @@ public void PreserveHttpOriginEmbeddedInConverterUrl()
93117 [ TestCase ( "http://[::1]:8000/x" , false ) ]
94118 [ TestCase ( "https://peer.decentraland.org/x" , false ) ]
95119 [ TestCase ( "file:///tmp/streaming-asset.bin" , false ) ]
96- public void ClassifyForbiddenCleartextForTheRedirectGuard ( string url , bool forbidden )
120+ public void ClassifyForbiddenCleartext ( string url , bool forbidden )
97121 {
98122 Assert . That ( WebRequestUtils . IsForbiddenCleartext ( url ) , Is . EqualTo ( forbidden ) ) ;
99123 }
100124
125+ [ TestCase ( "https://peer.decentraland.org/x" , "http://peer.decentraland.org/x" , true ) ]
126+ [ TestCase ( "http://127.0.0.1:8000/x" , "http://192.168.1.50:8000/x" , true ) ]
127+ [ TestCase ( "http://192.168.1.50:8000/api" , "http://192.168.1.50:8000/api" , false ) ]
128+ [ TestCase ( "http://192.168.1.50:8000/x" , "http://10.0.0.7:9000/y" , false ) ]
129+ [ TestCase ( "https://peer.decentraland.org/x" , "https://cdn.decentraland.org/x" , false ) ]
130+ [ TestCase ( "https://peer.decentraland.org/x" , "http://127.0.0.1:8000/x" , false ) ]
131+ public void ClassifyCleartextDowngradeForTheRedirectGuard ( string sentUrl , string finalUrl , bool downgrade )
132+ {
133+ Assert . That ( WebRequestUtils . IsCleartextDowngrade ( sentUrl , finalUrl ) , Is . EqualTo ( downgrade ) ) ;
134+ }
135+
101136 [ Test ]
102137 public void KeepPlayerSettingAlwaysAllowed ( )
103138 {
@@ -111,7 +146,7 @@ public void KeepPlayerSettingAlwaysAllowed()
111146 /// (WebRequestController.SendAsync -> InitializedWebRequest) and returns the URL the
112147 /// UnityWebRequest would actually be sent with. The request is never sent.
113148 /// </summary>
114- private static string UrlAfterEnvelopeInitialization ( string url )
149+ private static string UrlAfterEnvelopeInitialization ( string url , WebRequestSignInfo ? signInfo = null )
115150 {
116151 using var envelope = new RequestEnvelope < GenericGetRequest , GenericGetArguments > (
117152 GenericGetRequest . Initialize ,
@@ -120,9 +155,9 @@ private static string UrlAfterEnvelopeInitialization(string url)
120155 CancellationToken . None ,
121156 ReportData . UNSPECIFIED ,
122157 WebRequestHeadersInfo . NewEmpty ( ) ,
123- signInfo : null ) ;
158+ signInfo ) ;
124159
125- GenericGetRequest request = envelope . InitializedWebRequest ( Substitute . For < IWeb3IdentityCache > ( ) ) ;
160+ GenericGetRequest request = envelope . InitializedWebRequest ( SigningIdentityCache ( ) ) ;
126161 using UnityWebRequest unityWebRequest = request . UnityWebRequest ;
127162 return unityWebRequest . url ;
128163 }
@@ -145,11 +180,25 @@ private static string TextureUrlAfterEnvelopeInitialization(string url, bool ktx
145180 WebRequestHeadersInfo . NewEmpty ( ) ,
146181 signInfo : null ) ;
147182
148- GetTextureWebRequest request = envelope . InitializedWebRequest ( Substitute . For < IWeb3IdentityCache > ( ) ) ;
183+ GetTextureWebRequest request = envelope . InitializedWebRequest ( SigningIdentityCache ( ) ) ;
149184 using UnityWebRequest unityWebRequest = request . UnityWebRequest ;
150185 return unityWebRequest . url ;
151186 }
152187
188+ /// <summary>
189+ /// An identity cache whose identity signs any payload with an empty auth chain, so
190+ /// signed envelopes can be initialized without a real wallet.
191+ /// </summary>
192+ private static IWeb3IdentityCache SigningIdentityCache ( )
193+ {
194+ IWeb3Identity identity = Substitute . For < IWeb3Identity > ( ) ;
195+ identity . Sign ( Arg . Any < string > ( ) ) . Returns ( _ => AuthChain . Create ( ) ) ;
196+
197+ IWeb3IdentityCache cache = Substitute . For < IWeb3IdentityCache > ( ) ;
198+ cache . Identity . Returns ( identity ) ;
199+ return cache ;
200+ }
201+
153202 /// <summary>
154203 /// UnityWebRequest applies its own URL canonicalization; comparing against the same
155204 /// canonicalization keeps the assertions about the scheme policy only.
0 commit comments