From 28151b573fab8e89f8e87604e8de967136404f9d Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Thu, 22 Jun 2023 14:22:34 +0200 Subject: [PATCH] add support for https proxy (#87638) * add support for https proxy * winhttp * https * diag --- .../Net/Http/HttpClientHandlerTest.Proxy.cs | 6 +-- .../src/Resources/Strings.resx | 2 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 15 +++++++ .../HttpConnectionPoolManager.cs | 2 +- .../HttpEnvironmentProxy.cs | 8 +++- .../Http/SocketsHttpHandler/HttpUtilities.cs | 2 +- .../tests/FunctionalTests/DiagnosticsTests.cs | 6 +-- .../FunctionalTests/SocketsHttpHandlerTest.cs | 41 ++++++++++++++++++- .../UnitTests/HttpEnvironmentProxyTest.cs | 2 + 9 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs index 8ab1e35742621..1946228bf7fad 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs @@ -305,7 +305,7 @@ public async Task AuthenticatedProxyTunnelRequest_PostAsyncWithNoCreds_Throws() } } - [Fact] + [ConditionalFact(nameof(HttpClientHandlerTestBase.IsWinHttpHandler))] public async Task Proxy_SslProxyUnsupported_Throws() { using (HttpClientHandler handler = CreateHttpClientHandler()) @@ -313,9 +313,7 @@ public async Task Proxy_SslProxyUnsupported_Throws() { handler.Proxy = new WebProxy($"https://{Guid.NewGuid():N}"); - Type expectedType = IsWinHttpHandler ? typeof(HttpRequestException) : typeof(NotSupportedException); - - await Assert.ThrowsAsync(expectedType, () => client.GetAsync($"http://{Guid.NewGuid():N}")); + await Assert.ThrowsAsync(() => client.GetAsync($"http://{Guid.NewGuid():N}")); } } diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index baa2d519fc4f5..e7c2b65366259 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -370,7 +370,7 @@ Cannot access a closed stream. - Only the 'http', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies. + Only the 'http', 'https', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies. Request headers must contain only ASCII characters. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index c415f5c687d53..c281b29e5610c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -114,6 +114,7 @@ internal sealed class HttpConnectionPool : IDisposable private readonly SslClientAuthenticationOptions? _sslOptionsHttp2; private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only; private SslClientAuthenticationOptions? _sslOptionsHttp3; + private SslClientAuthenticationOptions? _sslOptionsProxy; /// Whether the pool has been used since the last time a cleanup occurred. private bool _usedSinceLastCleanup = true; @@ -302,6 +303,12 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK _http2RequestQueue = new RequestQueue(); } + if (_proxyUri != null && HttpUtilities.IsSupportedSecureScheme(_proxyUri.Scheme)) + { + _sslOptionsProxy = ConstructSslOptions(poolManager, _proxyUri.IdnHost); + _sslOptionsProxy.ApplicationProtocols = null; + } + if (NetEventSource.Log.IsEnabled()) Trace($"{this}"); } @@ -1525,10 +1532,18 @@ public ValueTask SendAsync(HttpRequestMessage request, bool case HttpConnectionKind.ProxyConnect: Debug.Assert(_originAuthority != null); stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false); + if (_kind == HttpConnectionKind.ProxyConnect && _sslOptionsProxy != null) + { + stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, stream, cancellationToken).ConfigureAwait(false); + } break; case HttpConnectionKind.Proxy: stream = await ConnectToTcpHostAsync(_proxyUri!.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); + if (_sslOptionsProxy != null) + { + stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, stream, cancellationToken).ConfigureAwait(false); + } break; case HttpConnectionKind.ProxyTunnel: diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index db54690e4ac40..a2a39db41aaaa 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -25,7 +25,7 @@ namespace System.Net.Http // (8) HttpConnection.SendAsyncCore: Write request to connection and read response // Also, handle cookie processing // - // Redirect and deompression handling are done above HttpConnectionPoolManager, + // Redirect and decompression handling are done above HttpConnectionPoolManager, // in RedirectHandler and DecompressionHandler respectively. /// Provides a set of connection pools, each for its own endpoint. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs index 43b12177bed0c..7f790d4b95745 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs @@ -128,11 +128,18 @@ private HttpEnvironmentProxy(Uri? httpProxy, Uri? httpsProxy, string? bypassList int hostIndex = 0; string protocol = "http"; + ushort port = 80; if (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) { hostIndex = 7; } + else if (value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + hostIndex = 8; + protocol = "https"; + port = 443; + } else if (value.StartsWith("socks4://", StringComparison.OrdinalIgnoreCase)) { hostIndex = 9; @@ -156,7 +163,6 @@ private HttpEnvironmentProxy(Uri? httpProxy, Uri? httpsProxy, string? bypassList string? user = null; string? password = null; - ushort port = 80; string host; // Check if there is authentication part with user and possibly password. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs index 1c2f442d3e21a..be47d560ea77e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs @@ -22,7 +22,7 @@ internal static bool IsSecureWebSocketScheme(string scheme) => string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase); internal static bool IsSupportedProxyScheme(string scheme) => - string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme); + string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme); internal static bool IsSocksScheme(string scheme) => string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) || diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs index 7dacb64476d99..a3a83db4ea6f1 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs @@ -663,10 +663,10 @@ public void SendAsync_ExpectedDiagnosticSynchronousExceptionActivityLogging() using (HttpClientHandler handler = CreateHttpClientHandler(useVersion)) using (HttpClient client = CreateHttpClient(handler, useVersion)) { - // Set a https proxy. + // Set a ftp proxy. // Forces a synchronous exception for SocketsHttpHandler. - // SocketsHttpHandler only allow http scheme for proxies. - handler.Proxy = new WebProxy($"https://foo.bar", false); + // SocketsHttpHandler only allow http & https & socks scheme for proxies. + handler.Proxy = new WebProxy($"ftp://foo.bar", false); var request = new HttpRequestMessage(HttpMethod.Get, InvalidUri) { Version = Version.Parse(useVersion) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index f33667e2d72e0..e365c7a72006f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -7,13 +7,11 @@ using System.IO.Pipes; using System.Linq; using System.Net.Http.Headers; -using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Net.Test.Common; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -656,6 +654,45 @@ public SocketsHttpHandler_HttpClientHandler_SslProtocols_Test(ITestOutputHelper public sealed class SocketsHttpHandler_HttpClientHandler_Proxy_Test : HttpClientHandler_Proxy_Test { public SocketsHttpHandler_HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Proxy_Https_Succeeds(bool secureUri) + { + var releaseServer = new TaskCompletionSource(); + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + bool validationCalled = false; + using SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true); + + handler.Proxy = new UseSpecifiedUriWebProxy(uri, new NetworkCredential("abc", "password")); + handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, error) => + { + validationCalled = true; + return true; + }; + + using (HttpClient client = CreateHttpClient(handler)) + { + HttpResponseMessage response = await client.GetAsync(secureUri ? "https://foo.bar/" : "http://foo.bar/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(validationCalled); + + } + }, server => server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAndSendResponseAsync(); + if (secureUri) + { + // client will send CONNECT and if that succeeds it will negotiate TLS + + var sslConnection = await LoopbackServer.Connection.CreateAsync(null, connection.Stream, new LoopbackServer.Options { UseSsl = true }); + await sslConnection.ReadRequestHeaderAndSendResponseAsync(); + } + }), + new LoopbackServer.Options { UseSsl = true }); + } } public abstract class SocketsHttpHandler_TrailingHeaders_Test : HttpClientHandlerTestBase diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs index 715e70a6d465c..727e4f58d6908 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs @@ -142,6 +142,8 @@ public void HttpProxy_EnvironmentProxy_Loaded() [InlineData("socks4://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] [InlineData("socks4a://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] [InlineData("socks5://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] + [InlineData("https://1.1.1.5:3005", "1.1.1.5", "3005", null, null)] + [InlineData("https://1.1.1.5", "1.1.1.5", "443", null, null)] public void HttpProxy_Uri_Parsing(string _input, string _host, string _port, string _user, string _password) { RemoteExecutor.Invoke((input, host, port, user, password) =>