@@ -278,24 +278,125 @@ await LoopbackServer.CreateServerAsync(async (proxyServer, proxyUri) =>
278278 await connection . SendResponseAsync ( HttpStatusCode . OK ) . ConfigureAwait ( false ) ;
279279 } ) ;
280280
281- await RemoteExecutor . Invoke ( async ( proxyUriString , username , password , useVersionString ) =>
281+ // Encode proxy URI with embedded credentials so we stay within RemoteExecutor's 3-arg limit
282+ string proxyUriWithCreds = $ "http://{ ExpectedUsername } :{ ExpectedPassword } @{ proxyUri . Host } :{ proxyUri . Port } ";
283+
284+ await RemoteExecutor . Invoke ( async ( proxyUriString , useVersionString ) =>
282285 {
286+ var proxyUriParsed = new Uri ( proxyUriString ) ;
283287 using HttpClientHandler handler = CreateHttpClientHandler ( useVersionString ) ;
284- handler . Proxy = new WebProxy ( new Uri ( proxyUriString ) )
288+ handler . Proxy = new WebProxy ( new Uri ( $ "http:// { proxyUriParsed . Host } : { proxyUriParsed . Port } " ) )
285289 {
286- Credentials = new NetworkCredential ( username , password )
290+ Credentials = new NetworkCredential (
291+ proxyUriParsed . UserInfo . Split ( ':' ) [ 0 ] ,
292+ proxyUriParsed . UserInfo . Split ( ':' ) [ 1 ] )
287293 } ;
288294
289295 using HttpClient client = CreateHttpClient ( handler , useVersionString ) ;
290296 using HttpResponseMessage response = await client . GetAsync ( "http://destination.test/" ) ;
291297
292298 Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
293- } , proxyUri . ToString ( ) , ExpectedUsername , ExpectedPassword , UseVersion . ToString ( ) ,
299+ } , proxyUriWithCreds , UseVersion . ToString ( ) ,
294300 new RemoteInvokeOptions { StartInfo = psi } ) . DisposeAsync ( ) ;
295301
296302 await serverTask ;
297303 } ) ;
298304 }
305+
306+ /// <summary>
307+ /// Tests proactive proxy auth across 4 proxy/request protocol combinations:
308+ /// HTTP proxy + HTTP request, HTTP proxy + HTTPS request (CONNECT tunnel),
309+ /// HTTPS proxy + HTTP request, HTTPS proxy + HTTPS request (CONNECT tunnel).
310+ /// Verifies that the Proxy-Authorization header is present on the first request to the proxy.
311+ /// </summary>
312+ [ ConditionalTheory ( typeof ( RemoteExecutor ) , nameof ( RemoteExecutor . IsSupported ) ) ]
313+ [ InlineData ( false , false ) ] // HTTP proxy + HTTP request
314+ [ InlineData ( false , true ) ] // HTTP proxy + HTTPS request (CONNECT tunnel)
315+ [ InlineData ( true , false ) ] // HTTPS proxy + HTTP request
316+ [ InlineData ( true , true ) ] // HTTPS proxy + HTTPS request (CONNECT tunnel)
317+ public async Task ProxyAuth_ProxyAndRequestProtocolCombinations_SentProactively ( bool proxyUseSsl , bool requestUseSsl )
318+ {
319+ const string ExpectedUsername = "matrixuser" ;
320+ const string ExpectedPassword = "matrixpass" ;
321+ string expectedToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $ "{ ExpectedUsername } :{ ExpectedPassword } ") ) ;
322+
323+ var proxyOptions = new LoopbackServer . Options { UseSsl = proxyUseSsl } ;
324+
325+ await LoopbackServer . CreateServerAsync ( async ( proxyServer , proxyUri ) =>
326+ {
327+ var psi = new ProcessStartInfo ( ) ;
328+ psi . Environment . Add ( ProxyPreAuthEnvVar , "1" ) ;
329+
330+ Task serverTask = proxyServer . AcceptConnectionAsync ( async connection =>
331+ {
332+ // Read the first request sent to the proxy
333+ List < string > lines = await connection . ReadRequestHeaderAsync ( ) . ConfigureAwait ( false ) ;
334+
335+ // Verify the Proxy-Authorization header is present on the first request
336+ string ? authHeader = null ;
337+ foreach ( string line in lines )
338+ {
339+ if ( line . StartsWith ( "Proxy-Authorization:" , StringComparison . OrdinalIgnoreCase ) )
340+ {
341+ authHeader = line ;
342+ break ;
343+ }
344+ }
345+
346+ Assert . NotNull ( authHeader ) ;
347+ Assert . Contains ( "Basic" , authHeader ) ;
348+ Assert . Contains ( expectedToken , authHeader ) ;
349+
350+ if ( requestUseSsl )
351+ {
352+ // For HTTPS request, the proxy received a CONNECT request.
353+ // Verify it's a CONNECT method.
354+ Assert . StartsWith ( "CONNECT" , lines [ 0 ] ) ;
355+
356+ // Send 200 to establish the tunnel
357+ await connection . SendResponseAsync ( HttpStatusCode . OK ) . ConfigureAwait ( false ) ;
358+
359+ // Now the client will negotiate TLS through the tunnel.
360+ // Wrap the connection's stream in SSL to act as the destination server.
361+ var sslConnection = await LoopbackServer . Connection . CreateAsync (
362+ null , connection . Stream , new LoopbackServer . Options { UseSsl = true } ) ;
363+ await sslConnection . ReadRequestHeaderAndSendResponseAsync ( HttpStatusCode . OK ) . ConfigureAwait ( false ) ;
364+ }
365+ else
366+ {
367+ // For HTTP request, the proxy received a plain GET request.
368+ Assert . StartsWith ( "GET" , lines [ 0 ] ) ;
369+ await connection . SendResponseAsync ( HttpStatusCode . OK ) . ConfigureAwait ( false ) ;
370+ }
371+ } ) ;
372+
373+ string requestScheme = requestUseSsl ? "https" : "http" ;
374+
375+ // Encode proxy URI with embedded credentials so we stay within RemoteExecutor's 3-arg limit
376+ string proxyScheme = proxyUseSsl ? "https" : "http" ;
377+ string proxyUriWithCreds = $ "{ proxyScheme } ://{ ExpectedUsername } :{ ExpectedPassword } @{ proxyUri . Host } :{ proxyUri . Port } ";
378+
379+ await RemoteExecutor . Invoke ( async ( proxyUriString , reqScheme , useVersionString ) =>
380+ {
381+ var proxyUriParsed = new Uri ( proxyUriString ) ;
382+ using HttpClientHandler handler = CreateHttpClientHandler ( useVersionString ) ;
383+ handler . Proxy = new WebProxy ( new Uri ( $ "{ proxyUriParsed . Scheme } ://{ proxyUriParsed . Host } :{ proxyUriParsed . Port } ") )
384+ {
385+ Credentials = new NetworkCredential (
386+ proxyUriParsed . UserInfo . Split ( ':' ) [ 0 ] ,
387+ proxyUriParsed . UserInfo . Split ( ':' ) [ 1 ] )
388+ } ;
389+
390+ using HttpClient client = CreateHttpClient ( handler , useVersionString ) ;
391+ using HttpResponseMessage response = await client . GetAsync ( $ "{ reqScheme } ://destination.test/") ;
392+
393+ Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
394+ } , proxyUriWithCreds , requestScheme , UseVersion . ToString ( ) ,
395+ new RemoteInvokeOptions { StartInfo = psi } ) . DisposeAsync ( ) ;
396+
397+ await serverTask ;
398+ } , proxyOptions ) ;
399+ }
299400 }
300401
301402 public sealed class ProactiveProxyAuthTest_Http11 : ProactiveProxyAuthTest
0 commit comments