Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for https proxy #87638

Merged
merged 5 commits into from Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -305,17 +305,15 @@ public async Task AuthenticatedProxyTunnelRequest_PostAsyncWithNoCreds_Throws()
}
}

[Fact]
[ConditionalFact(nameof(HttpClientHandlerTestBase.IsWinHttpHandler))]
public async Task Proxy_SslProxyUnsupported_Throws()
{
using (HttpClientHandler handler = CreateHttpClientHandler())
using (HttpClient client = CreateHttpClient(handler))
{
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<HttpRequestException>(() => client.GetAsync($"http://{Guid.NewGuid():N}"));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.Net.Http/src/Resources/Strings.resx
Expand Up @@ -370,7 +370,7 @@
<value>Cannot access a closed stream.</value>
</data>
<data name="net_http_invalid_proxy_scheme" xml:space="preserve">
<value>Only the 'http', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies.</value>
<value>Only the 'http', 'https', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies.</value>
</data>
<data name="net_http_request_invalid_char_encoding" xml:space="preserve">
<value>Request headers must contain only ASCII characters.</value>
Expand Down
Expand Up @@ -114,6 +114,7 @@ internal sealed class HttpConnectionPool : IDisposable
private readonly SslClientAuthenticationOptions? _sslOptionsHttp2;
private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only;
private SslClientAuthenticationOptions? _sslOptionsHttp3;
private SslClientAuthenticationOptions? _sslOptionsProxy;

/// <summary>Whether the pool has been used since the last time a cleanup occurred.</summary>
private bool _usedSinceLastCleanup = true;
Expand Down Expand Up @@ -302,6 +303,12 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
_http2RequestQueue = new RequestQueue<Http2Connection?>();
}

if (_proxyUri != null && HttpUtilities.IsSupportedSecureScheme(_proxyUri.Scheme))
{
_sslOptionsProxy = ConstructSslOptions(poolManager, _proxyUri.IdnHost);
_sslOptionsProxy.ApplicationProtocols = null;
}

if (NetEventSource.Log.IsEnabled()) Trace($"{this}");
}

Expand Down Expand Up @@ -1525,10 +1532,18 @@ private async ValueTask<(Stream, TransportContext?)> ConnectAsync(HttpRequestMes
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:
Expand Down
Expand Up @@ -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.

/// <summary>Provides a set of connection pools, each for its own endpoint.</summary>
Expand Down
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
Expand Up @@ -22,7 +22,7 @@ internal static class HttpUtilities
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);
wfurt marked this conversation as resolved.
Show resolved Hide resolved

internal static bool IsSocksScheme(string scheme) =>
string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) ||
Expand Down
Expand Up @@ -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)
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -656,6 +654,45 @@ public sealed class SocketsHttpHandler_HttpClientHandler_SslProtocols_Test : Htt
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
Expand Down
Expand Up @@ -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) =>
Expand Down