Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public byte[] Http2AltSvcOriginUri

private bool TryGetPooledHttp2Connection(HttpRequestMessage request, [NotNullWhen(true)] out Http2Connection? connection, out HttpConnectionWaiter<Http2Connection?>? waiter)
{
Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http || _kind == HttpConnectionKind.SocksTunnel || _kind == HttpConnectionKind.SslSocksTunnel);
Debug.Assert(_kind is HttpConnectionKind.Https or HttpConnectionKind.SslProxyTunnel or HttpConnectionKind.Http or HttpConnectionKind.ProxyTunnel or HttpConnectionKind.SocksTunnel or HttpConnectionKind.SslSocksTunnel);

// Look for a usable connection.
while (true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
Debug.Assert(sslHostName == null);
Debug.Assert(proxyUri != null);

_http2Enabled = false;
// A CONNECT tunnel to the origin server behaves like a direct connection once established,
// so cleartext HTTP/2 (h2c) can be used over it. HTTP/1.1 WebSockets keep working because
// the WebSocket upgrade request uses HTTP/1.1 and never attempts HTTP/2.
Comment thread
MihaZupan marked this conversation as resolved.
_http3Enabled = false;
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ internal enum HttpConnectionKind : byte
{
Http, // Non-secure connection with no proxy.
Https, // Secure connection with no proxy.
Proxy, // HTTP proxy usage for non-secure (HTTP) requests.
ProxyTunnel, // Non-secure websocket (WS) connection using CONNECT tunneling through proxy.
Proxy, // HTTP proxy usage for non-secure (HTTP) requests using HTTP/1.1.
ProxyTunnel, // Non-secure (HTTP/WS) connection using CONNECT tunneling through proxy. Used for cleartext HTTP/2 (h2c) and non-secure WebSockets.
SslProxyTunnel, // HTTP proxy usage for secure (HTTPS/WSS) requests using SSL and proxy CONNECT.
ProxyConnect, // Connection used for proxy CONNECT. Tunnel will be established on top of this.
SocksTunnel, // SOCKS proxy usage for HTTP requests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,13 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox
}
else if (sslHostName == null)
{
if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme))
// Both non-secure WebSockets (WS) and cleartext HTTP/2 (h2c) need a CONNECT tunnel to the destination,
// because they can't be expressed using the absolute-form request line an HTTP proxy expects.
// h2c is only tunneled when HTTP/2 is required or preferred; requests that allow downgrading to
// HTTP/1.1 (RequestVersionOrLower) keep using the shared HTTP/1.1 proxy pool below.
if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme) ||
(request.Version.Major == 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower))
{
// Non-secure websocket connection through proxy to the destination.
return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,62 @@ await LoopbackServer.CreateClientAndServerAsync(async uri =>
}),
new LoopbackServer.Options { UseSsl = true });
}

[ConditionalTheory(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))]
[InlineData("1.1", HttpVersionPolicy.RequestVersionExact, false)]
[InlineData("1.1", HttpVersionPolicy.RequestVersionOrHigher, false)]
[InlineData("2.0", HttpVersionPolicy.RequestVersionExact, false)]
[InlineData("2.0", HttpVersionPolicy.RequestVersionOrHigher, false)]
[InlineData("2.0", HttpVersionPolicy.RequestVersionOrLower, false)]
[InlineData("1.1", HttpVersionPolicy.RequestVersionExact, true)]
[InlineData("2.0", HttpVersionPolicy.RequestVersionExact, true)]
[InlineData("2.0", HttpVersionPolicy.RequestVersionOrLower, true)]
public async Task ProxiedRequest_UsesConnectTunnelForHttpsAndCleartextHttp2(string version, HttpVersionPolicy versionPolicy, bool useSsl)
{
Version requestVersion = Version.Parse(version);

if (useSsl && requestVersion.Major == 2 && !PlatformDetection.SupportsAlpn)
{
// Negotiating HTTP/2 over TLS requires ALPN.
return;
}
Comment thread
MihaZupan marked this conversation as resolved.

// HTTPS requests always tunnel through the proxy using CONNECT. Cleartext (http://) requests are
// only tunneled when they require HTTP/2 (h2c, which can't use the proxy's absolute-form request line);
// otherwise they are forwarded using an absolute-form HTTP/1.1 request line.
bool useHttp2 = requestVersion.Major == 2 && (versionPolicy != HttpVersionPolicy.RequestVersionOrLower || useSsl);
bool expectConnectTunnel = useSsl || useHttp2;
Version expectedResponseVersion = useHttp2 ? HttpVersion.Version20 : HttpVersion.Version11;

using LoopbackProxyServer proxy = LoopbackProxyServer.Create();

await GetFactoryForVersion(expectedResponseVersion).CreateClientAndServerAsync(
async uri =>
{
using SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
handler.Proxy = new UseSpecifiedUriWebProxy(proxy.Uri);
using HttpClient client = CreateHttpClient(handler);

HttpRequestMessage request = new(HttpMethod.Get, uri)
{
Version = requestVersion,
VersionPolicy = versionPolicy
};

using HttpResponseMessage response = await client.SendAsync(TestAsync, request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedResponseVersion, response.Version);
Assert.Equal("Echo", await response.Content.ReadAsStringAsync());
},
async server => await server.HandleRequestAsync(content: "Echo"),
options: new GenericLoopbackOptions { UseSsl = useSsl });

Assert.NotEmpty(proxy.Requests);

// A tunneled request must have used a CONNECT request; a forwarded request must have used an
// absolute-form HTTP/1.1 request line.
Assert.All(proxy.Requests, r => Assert.StartsWith(expectConnectTunnel ? "CONNECT " : "GET http://", r.RequestLine));
}
}

[SkipOnPlatform(TestPlatforms.Wasi, "SocketsHttpHandler is not supported on WASI")]
Expand Down
Loading