diff --git a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs index da6e915a237fcc..f4f92c0791c8f5 100644 --- a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs @@ -130,6 +130,7 @@ public class HttpRequestData public byte[] Body; public string Method; public string Path; + public Version Version; public List Headers { get; } public int RequestId; // Generic request ID. Currently only used for HTTP/2 to hold StreamId. @@ -143,6 +144,7 @@ public static async Task FromHttpRequestMessageAsync(System.Net var result = new HttpRequestData(); result.Method = request.Method.ToString(); result.Path = request.RequestUri?.AbsolutePath; + result.Version = request.Version; foreach (var header in request.Headers) { diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 4701cecd248fba..80705c4dd78777 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -566,6 +566,7 @@ public async Task ReadBodyAsync(bool expectEndOfStream = false) // Extract method and path requestData.Method = requestData.GetSingleHeaderValue(":method"); requestData.Path = requestData.GetSingleHeaderValue(":path"); + requestData.Version = HttpVersion20.Value; if (readBody && (frame.Flags & FrameFlags.EndStream) == 0) { diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs index b290c5a074ab88..06f8c4633268c0 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs @@ -86,4 +86,9 @@ public override Task CreateConnectionAsync(Socket soc throw new NotImplementedException("HTTP/3 does not operate over a Socket."); } } + + public static class HttpVersion30 + { + public static readonly Version Value = new Version(3, 0); + } } diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs index d0e8d433ec7098..3286aff03a282e 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs @@ -201,6 +201,7 @@ private HttpRequestData ParseHeaders(ReadOnlySpan buffer) break; } } + request.Version = HttpVersion30.Value; return request; } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs index 6a6657b3adb025..7e0f4e43838b5b 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs @@ -54,77 +54,109 @@ public override void Dispose() _listenSocket = null; } } + public override async Task EstablishGenericConnectionAsync() { Socket socket = await _listenSocket.AcceptAsync().ConfigureAwait(false); Stream stream = new NetworkStream(socket, ownsSocket: true); - if (_options.UseSsl) + var options = new GenericLoopbackOptions() { - var sslStream = new SslStream(stream, false, delegate { return true; }); - - using (X509Certificate2 cert = Configuration.Certificates.GetServerCertificate()) - { - SslServerAuthenticationOptions options = new SslServerAuthenticationOptions(); - - options.EnabledSslProtocols = _options.SslProtocols; - - var protocols = new List(); - protocols.Add(SslApplicationProtocol.Http11); - protocols.Add(SslApplicationProtocol.Http2); - options.ApplicationProtocols = protocols; + Address = _options.Address, + SslProtocols = _options.SslProtocols, + UseSsl = false, + ListenBacklog = _options.ListenBacklog + }; - options.ServerCertificate = cert; + GenericLoopbackConnection connection = null; - await sslStream.AuthenticateAsServerAsync(options, CancellationToken.None).ConfigureAwait(false); + try + { + if (_options.UseSsl) + { + var sslStream = new SslStream(stream, false, delegate { return true; }); + + using (X509Certificate2 cert = Configuration.Certificates.GetServerCertificate()) + { + SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions(); + + sslOptions.EnabledSslProtocols = _options.SslProtocols; + sslOptions.ApplicationProtocols = _options.SslApplicationProtocols; + sslOptions.ServerCertificate = cert; + + await sslStream.AuthenticateAsServerAsync(sslOptions, CancellationToken.None).ConfigureAwait(false); + } + + stream = sslStream; + if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) + { + // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again. + return connection = await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false); + } + if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http11 || + sslStream.NegotiatedApplicationProtocol == default) + { + // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again. + return connection = await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false); + } + else + { + throw new Exception($"Unsupported negotiated protocol {sslStream.NegotiatedApplicationProtocol}"); + } } - stream = sslStream; - if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2) + if (_options.ClearTextVersion is null) { - // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again. - return await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream); + throw new Exception($"HTTP server does not accept clear text connections, either set '{nameof(HttpAgnosticOptions.UseSsl)}' or set up '{nameof(HttpAgnosticOptions.ClearTextVersion)}' in server options."); } - if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http11) + + var buffer = new byte[24]; + var position = 0; + while (position < buffer.Length) { - // Do not pass original options so the CreateConnectionAsync won't try to do ALPN again. - return await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream); + var readBytes = await stream.ReadAsync(buffer, position, buffer.Length - position).ConfigureAwait(false); + if (readBytes == 0) + { + break; + } + position += readBytes; } - throw new Exception($"Unsupported negotiated protocol {sslStream.NegotiatedApplicationProtocol}"); - } + + var memory = new Memory(buffer, 0, position); + stream = new ReturnBufferStream(stream, memory); - var buffer = new byte[24]; - var position = 0; - while (position < buffer.Length) - { - var readBytes = await stream.ReadAsync(buffer, position, buffer.Length - position).ConfigureAwait(false); - if (readBytes == 0) + var prefix = Text.Encoding.ASCII.GetString(memory.Span); + if (prefix == Http2LoopbackConnection.Http2Prefix) { - break; + if (_options.ClearTextVersion == HttpVersion.Version20 || _options.ClearTextVersion == HttpVersion.Unknown) + { + return connection = await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false); + } } - position += readBytes; - } - - var memory = new Memory(buffer, 0, position); - stream = new ReturnBufferStream(stream, memory); - - var prefix = Text.Encoding.ASCII.GetString(memory.Span); - if (prefix == Http2LoopbackConnection.Http2Prefix) - { - if (_options.ClearTextVersion == HttpVersion.Version20 || _options.ClearTextVersion == HttpVersion.Unknown) + else { - return await Http2LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream); + if (_options.ClearTextVersion == HttpVersion.Version11 || _options.ClearTextVersion == HttpVersion.Unknown) + { + return connection = await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream, options).ConfigureAwait(false); + } } + + throw new Exception($"HTTP/{_options.ClearTextVersion} server cannot establish connection due to unexpected data: '{prefix}'"); + } + catch + { + connection?.Dispose(); + connection = null; + stream.Dispose(); + throw; } - else + finally { - if (_options.ClearTextVersion == HttpVersion.Version11 || _options.ClearTextVersion == HttpVersion.Unknown) + if (connection != null) { - return await Http11LoopbackServerFactory.Singleton.CreateConnectionAsync(socket, stream); + await connection.InitializeConnectionAsync().ConfigureAwait(false); } } - - throw new Exception($"HTTP/{_options.ClearTextVersion} server cannot establish connection due to unexpected data: '{prefix}'"); } public override async Task HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList headers = null, string content = "") @@ -162,12 +194,10 @@ public static async Task CreateClientAndServerAsync(Func clientFunc, public class HttpAgnosticOptions : GenericLoopbackOptions { + // Default null will raise an exception for any clear text protocol version + // Use HttpVersion.Unknown to use protocol version detection for clear text. public Version ClearTextVersion { get; set; } - - public HttpAgnosticOptions() - { - ClearTextVersion = HttpVersion.Version11; - } + public List SslApplicationProtocols { get; set; } } public sealed class HttpAgnosticLoopbackServerFactory : LoopbackServerFactory @@ -259,5 +289,9 @@ public override int Read(byte[] buffer, int offset, int count) public override void SetLength(long value) => _stream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) => _stream.Write(buffer, offset, count); + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + } } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index 371c47f35527d2..e889a86a8ea243 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -266,6 +266,7 @@ public async Task GetAsync_IPBasedUri_Success(IPAddress address) using HttpClient client = CreateHttpClient(handler); var options = new GenericLoopbackOptions { Address = address }; + await LoopbackServerFactory.CreateServerAsync(async (server, url) => { _output.WriteLine(url.ToString()); diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 2c027ca702cd41..bff8be28d144c5 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -717,6 +717,7 @@ public override async Task ReadRequestDataAsync(bool readBody = string[] splits = Encoding.ASCII.GetString(headerLines[0]).Split(' '); requestData.Method = splits[0]; requestData.Path = splits[1]; + requestData.Version = Version.Parse(splits[2].Substring(splits[2].IndexOf('/') + 1)); // Convert header lines to key/value pairs // Skip first line since it's the status line diff --git a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs index 9cca3a6466d216..041d6efe1e705f 100644 --- a/src/libraries/Common/tests/System/Net/Http/TestHelper.cs +++ b/src/libraries/Common/tests/System/Net/Http/TestHelper.cs @@ -112,54 +112,6 @@ public static IPAddress GetIPv6LinkLocalAddress() => .Where(a => a.IsIPv6LinkLocal) .FirstOrDefault(); - public static void EnableUnencryptedHttp2IfNecessary(HttpClientHandler handler) - { - if (PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback()) - { - return; - } - - FieldInfo socketsHttpHandlerField = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.NonPublic | BindingFlags.Instance); - if (socketsHttpHandlerField == null) - { - // Not using .NET Core implementation, i.e. could be .NET Framework. - return; - } - - object socketsHttpHandler = socketsHttpHandlerField.GetValue(handler); - Assert.NotNull(socketsHttpHandler); - - EnableUncryptedHttp2(socketsHttpHandler); - } - -#if !NETFRAMEWORK - public static void EnableUnencryptedHttp2IfNecessary(SocketsHttpHandler socketsHttpHandler) - { - if (PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback()) - { - return; - } - - EnableUncryptedHttp2(socketsHttpHandler); - } -#endif - - private static void EnableUncryptedHttp2(object socketsHttpHandler) - { - // Get HttpConnectionSettings object from SocketsHttpHandler. - Type socketsHttpHandlerType = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.SocketsHttpHandler"); - FieldInfo settingsField = socketsHttpHandlerType.GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(settingsField); - object settings = settingsField.GetValue(socketsHttpHandler); - Assert.NotNull(settings); - - // Allow HTTP/2.0 via unencrypted socket if ALPN is not supported on platform. - Type httpConnectionSettingsType = typeof(HttpClientHandler).Assembly.GetType("System.Net.Http.HttpConnectionSettings"); - FieldInfo allowUnencryptedHttp2Field = httpConnectionSettingsType.GetField("_allowUnencryptedHttp2", BindingFlags.NonPublic | BindingFlags.Instance); - Assert.NotNull(allowUnencryptedHttp2Field); - allowUnencryptedHttp2Field.SetValue(settings, true); - } - public static byte[] GenerateRandomContent(int size) { byte[] data = new byte[size]; diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 415a52f71b17ad..dbf127857b1bd6 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -49,6 +49,7 @@ public HttpClient(System.Net.Http.HttpMessageHandler handler, bool disposeHandle public static System.Net.IWebProxy DefaultProxy { get { throw null; } set { } } public System.Net.Http.Headers.HttpRequestHeaders DefaultRequestHeaders { get { throw null; } } public System.Version DefaultRequestVersion { get { throw null; } set { } } + public System.Net.Http.HttpVersionPolicy DefaultVersionPolicy { get { throw null; } set { } } public long MaxResponseContentBufferSize { get { throw null; } set { } } public System.TimeSpan Timeout { get { throw null; } set { } } public void CancelPendingRequests() { } @@ -220,6 +221,7 @@ public HttpRequestMessage(System.Net.Http.HttpMethod method, System.Uri? request public HttpRequestOptions Options { get { throw null; } } public System.Uri? RequestUri { get { throw null; } set { } } public System.Version Version { get { throw null; } set { } } + public System.Net.Http.HttpVersionPolicy VersionPolicy { get { throw null; } set { } } public void Dispose() { } protected virtual void Dispose(bool disposing) { } public override string ToString() { throw null; } @@ -271,6 +273,12 @@ protected virtual void Dispose(bool disposing) { } public System.Net.Http.HttpResponseMessage EnsureSuccessStatusCode() { throw null; } public override string ToString() { throw null; } } + public enum HttpVersionPolicy + { + RequestVersionOrLower = 0, + RequestVersionOrHigher = 1, + RequestVersionExact = 2, + } public abstract partial class MessageProcessingHandler : System.Net.Http.DelegatingHandler { protected MessageProcessingHandler() { } diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 0ac22a4bf44cd3..61b45be26fdd93 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -567,4 +567,19 @@ The synchronous method is not supported by '{0}' for HTTP/2 or higher. Either use an asynchronous method or downgrade the request version to HTTP/1.1 or lower. + + HTTP request version upgrade is not enabled for synchronous '{0}'. Do not use '{1}' version policy for synchronous HTTP methods. + + + Requesting HTTP version {0} with version policy {1} while HTTP/{2} is not enabled. + + + Requesting HTTP version {0} with version policy {1} while unable to establish HTTP/{2} connection. + + + Requesting HTTP version {0} with version policy {1} while server returned HTTP/1.1 in ALPN. + + + Requesting HTTP version {0} with version policy {1} while server offers only version fallback. + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 420af9b298b7fc..455bc89df83013 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -52,6 +52,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs index a08edaf6c94824..dc3172c8a87728 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs @@ -26,6 +26,7 @@ public class HttpClient : HttpMessageInvoker private CancellationTokenSource _pendingRequestsCts; private HttpRequestHeaders? _defaultRequestHeaders; private Version _defaultRequestVersion = HttpUtilities.DefaultRequestVersion; + private HttpVersionPolicy _defaultVersionPolicy = HttpUtilities.DefaultVersionPolicy; private Uri? _baseAddress; private TimeSpan _timeout; @@ -57,6 +58,24 @@ public Version DefaultRequestVersion } } + /// + /// Gets or sets the default value of for implicitly created requests in convenience methods, + /// e.g.: , . + /// + /// + /// Note that this property has no effect on any of the and overloads + /// since they accept fully initialized . + /// + public HttpVersionPolicy DefaultVersionPolicy + { + get => _defaultVersionPolicy; + set + { + CheckDisposedOrStarted(); + _defaultVersionPolicy = value; + } + } + public Uri? BaseAddress { get { return _baseAddress; } @@ -803,7 +822,7 @@ private static void CheckBaseAddress(Uri? baseAddress, string parameterName) string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute); private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri? uri) => - new HttpRequestMessage(method, uri) { Version = _defaultRequestVersion }; + new HttpRequestMessage(method, uri) { Version = _defaultRequestVersion, VersionPolicy = _defaultVersionPolicy }; #endregion Private Helpers } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs index 83ee09fd178103..bad8cdf6424f1c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs @@ -22,6 +22,7 @@ public class HttpRequestMessage : IDisposable private Uri? _requestUri; private HttpRequestHeaders? _headers; private Version _version; + private HttpVersionPolicy _versionPolicy; private HttpContent? _content; private bool _disposed; private HttpRequestOptions? _options; @@ -41,6 +42,20 @@ public Version Version } } + /// + /// Gets or sets the policy determining how is interpreted and how is the final HTTP version negotiated with the server. + /// + public HttpVersionPolicy VersionPolicy + { + get { return _versionPolicy; } + set + { + CheckDisposed(); + + _versionPolicy = value; + } + } + public HttpContent? Content { get { return _content; } @@ -179,6 +194,7 @@ private void InitializeValues(HttpMethod method, Uri? requestUri) _method = method; _requestUri = requestUri; _version = HttpUtilities.DefaultRequestVersion; + _versionPolicy = HttpUtilities.DefaultVersionPolicy; } internal bool MarkAsSent() diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs index 189a25058c82a5..53edcaa3f90192 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs @@ -13,6 +13,8 @@ internal static class HttpUtilities internal static Version DefaultResponseVersion => HttpVersion.Version11; + internal static HttpVersionPolicy DefaultVersionPolicy => HttpVersionPolicy.RequestVersionOrLower; + internal static bool IsHttpUri(Uri uri) { Debug.Assert(uri != null); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpVersionPolicy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpVersionPolicy.cs new file mode 100644 index 00000000000000..9f3ddb5569f36b --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpVersionPolicy.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http +{ + /// + /// Determines behavior when selecting and negotiating HTTP version for a request. + /// + public enum HttpVersionPolicy + { + /// + /// Default behavior, either uses requested version or downgrades to a lower one. + /// + /// + /// If the server supports the requested version, either negotiated via ALPN (H2) or advertised via Alt-Svc (H3), + /// as well as a secure connection is being requested, the result is the . + /// Otherwise, downgrades to HTTP/1.1. + /// Note that this option does not allow use of prenegotiated clear text connection, e.g. H2C. + /// + RequestVersionOrLower, + + /// + /// Tries to uses highest available version, downgrading only to the requested version, not bellow. + /// Throwing if a connection with higher or equal version cannot be established. + /// + /// + /// If the server supports higher than requested version, either negotiated via ALPN (H2) or advertised via Alt-Svc (H3), + /// as well as secure connection is being requested, the result is the highest available one. + /// Otherwise, downgrades to the . + /// Note that this option allows to use prenegotiated clear text connection for the requested version but not for anything higher. + /// + RequestVersionOrHigher, + + /// + /// Uses only the requested version. + /// Throwing if a connection with the exact version cannot be established. + /// + /// + /// Note that this option allows to use prenegotiated clear text connection for the requested version. + /// + RequestVersionExact + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs index df8191b7402784..36a57147822a8c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs @@ -73,16 +73,6 @@ public bool LifetimeExpired(long nowTicks, TimeSpan lifetime) return expired; } - internal static HttpRequestException CreateRetryException() - { - // This is an exception that's thrown during request processing to indicate that the - // attempt to send the request failed in such a manner that the server is guaranteed to not have - // processed the request in any way, and thus the request can be retried. - // This will be caught in HttpConnectionPool.SendWithRetryAsync and the retry logic will kick in. - // The user should never see this exception. - throw new HttpRequestException(null, null, allowRetry: RequestRetryType.RetryOnSameOrNextProxy); - } - internal static bool IsDigit(byte c) => (uint)(c - '0') <= '9' - '0'; internal static int ParseStatusCode(ReadOnlySpan value) 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 e2c2e366424871..d83159a951b0bb 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 @@ -83,6 +83,7 @@ internal sealed class HttpConnectionPool : IDisposable /// Options specialized and cached for this pool and its key. private readonly SslClientAuthenticationOptions? _sslOptionsHttp11; private readonly SslClientAuthenticationOptions? _sslOptionsHttp2; + private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only; private readonly SslClientAuthenticationOptions? _sslOptionsHttp3; /// Queue of waiters waiting for a connection. Created on demand. @@ -116,11 +117,6 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK if (host != null) { _originAuthority = new HttpAuthority(host, port); - - if (_poolManager.Settings._assumePrenegotiatedHttp3ForTesting) - { - _http3Authority = _originAuthority; - } } _http2Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version20; @@ -133,7 +129,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(port != 0); Debug.Assert(sslHostName == null); Debug.Assert(proxyUri == null); - _http2Enabled = _poolManager.Settings._allowUnencryptedHttp2; + _http3Enabled = false; break; @@ -169,6 +165,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(port != 0); Debug.Assert(sslHostName != null); Debug.Assert(proxyUri != null); + _http3Enabled = false; // TODO: how do we tunnel HTTP3? break; @@ -222,6 +219,8 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK { _sslOptionsHttp2 = ConstructSslOptions(poolManager, sslHostName); _sslOptionsHttp2.ApplicationProtocols = s_http2ApplicationProtocols; + _sslOptionsHttp2Only = ConstructSslOptions(poolManager, sslHostName); + _sslOptionsHttp2Only.ApplicationProtocols = s_http2OnlyApplicationProtocols; // Note: // The HTTP/2 specification states: @@ -240,7 +239,6 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(hostHeader != null); _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader); _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader); - } if (_http3Enabled) @@ -261,6 +259,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK private static readonly List s_http3ApplicationProtocols = new List() { Http3Connection.Http3ApplicationProtocol }; private static readonly List s_http2ApplicationProtocols = new List() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }; + private static readonly List s_http2OnlyApplicationProtocols = new List() { SslApplicationProtocol.Http2 }; private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnectionPoolManager poolManager, string sslHostName) { @@ -290,8 +289,8 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public HttpAuthority? OriginAuthority => _originAuthority; public HttpConnectionSettings Settings => _poolManager.Settings; - public bool IsSecure => _sslOptionsHttp11 != null; public HttpConnectionKind Kind => _kind; + public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel; public bool AnyProxyKind => (_proxyUri != null); public Uri? ProxyUri => _proxyUri; public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; @@ -339,19 +338,60 @@ public byte[] Http2AltSvcOriginUri private ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> GetConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - if (_http3Enabled && request.Version.Major >= 3) + // Do not even attempt at getting/creating a connection if it's already obvious we cannot provided the one requested. + if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + if (request.Version.Major == 3 && !_http3Enabled) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_not_enabled, request.Version, request.VersionPolicy, 3))); + } + if (request.Version.Major == 2 && !_http2Enabled) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_not_enabled, request.Version, request.VersionPolicy, 2))); + } + } + + // Either H3 explicitly requested or secured upgraded allowed. + if (_http3Enabled && (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure))) { HttpAuthority? authority = _http3Authority; + // H3 is explicitly requested, assume prenegotiated H3. + if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + authority = authority ?? _originAuthority; + } if (authority != null) { + if (IsAltSvcBlocked(authority)) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3))); + } + return GetHttp3ConnectionAsync(request, authority, cancellationToken); } } + // If we got here, we cannot provide HTTP/3 connection. Do not continue if downgrade is not allowed. + if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3))); + } - if (_http2Enabled && request.Version.Major >= 2) + if (_http2Enabled && (request.Version.Major >= 2 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)) && + // If the connection is not secured and downgrade is possible, prefer HTTP/1.1. + (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower || IsSecure)) { return GetHttp2ConnectionAsync(request, async, cancellationToken); } + // If we got here, we cannot provide HTTP/2 connection. Do not continue if downgrade is not allowed. + if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 2))); + } return GetHttpConnectionAsync(request, async, cancellationToken); } @@ -535,7 +575,7 @@ public byte[] Http2AltSvcOriginUri HttpResponseMessage? failureResponse; (connection, transportContext, failureResponse) = - await ConnectAsync(request, async, true, cancellationToken).ConfigureAwait(false); + await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false); if (failureResponse != null) { @@ -610,6 +650,12 @@ public byte[] Http2AltSvcOriginUri { _http2Enabled = false; + if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + sslStream.Close(); + throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy)); + } + if (_associatedConnectionCount < _maxConnections) { IncrementConnectionCountNoLock(); @@ -750,7 +796,17 @@ private void AddHttp2Connection(Http2Connection newConnection) Trace("Attempting new HTTP3 connection."); } - QuicConnection quicConnection = await ConnectHelper.ConnectQuicAsync(authority.IdnHost, authority.Port, _sslOptionsHttp3, cancellationToken).ConfigureAwait(false); + QuicConnection quicConnection; + try + { + quicConnection = await ConnectHelper.ConnectQuicAsync(authority.IdnHost, authority.Port, _sslOptionsHttp3, cancellationToken).ConfigureAwait(false); + } + catch + { + // Disables HTTP/3 until server announces it can handle it via Alt-Svc. + BlocklistAuthority(authority); + throw; + } //TODO: NegotiatedApplicationProtocol not yet implemented. #if false @@ -817,13 +873,18 @@ public async ValueTask SendWithRetryAsync(HttpRequestMessag } catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnLowerHttpVersion) { + // Throw since fallback is not allowed by the version policy. + if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e); + } + if (NetEventSource.Log.IsEnabled()) { Trace($"Retrying request after exception on existing connection: {e}"); } // Eat exception and try again on a lower protocol version. - Debug.Assert(connection is HttpConnection == false, $"{nameof(RequestRetryType.RetryOnLowerHttpVersion)} should not be thrown by HTTP/1 connections."); request.Version = HttpVersion.Version11; continue; @@ -900,16 +961,10 @@ internal void HandleAltSvc(IEnumerable altSvcHeaderValues, TimeSpan? res { var authority = new HttpAuthority(value.Host!, value.Port); - if (_altSvcBlocklist != null) + if (IsAltSvcBlocked(authority)) { - lock (_altSvcBlocklist) - { - if (_altSvcBlocklist.Contains(authority)) - { - // Skip authorities in our blocklist. - continue; - } - } + // Skip authorities in our blocklist. + continue; } TimeSpan authorityMaxAge = value.MaxAge; @@ -993,6 +1048,22 @@ private void ExpireAltSvcAuthority() _http3Authority = null; } + /// + /// Checks whether the given is on the currext Alt-Svc blocklist. + /// + /// + private bool IsAltSvcBlocked(HttpAuthority authority) + { + if (_altSvcBlocklist != null) + { + lock (_altSvcBlocklist) + { + return _altSvcBlocklist.Contains(authority); + } + } + return false; + } + /// /// Blocklists an authority and resets the current authority back to origin. /// If the number of blocklisted authorities exceeds , @@ -1009,7 +1080,6 @@ private void ExpireAltSvcAuthority() internal void BlocklistAuthority(HttpAuthority badAuthority) { Debug.Assert(badAuthority != null); - Debug.Assert(badAuthority != _originAuthority); HashSet? altSvcBlocklist = _altSvcBlocklist; @@ -1136,7 +1206,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool return SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken); } - private async ValueTask<(Connection?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool async, bool allowHttp2, CancellationToken cancellationToken) + private async ValueTask<(Connection?, TransportContext?, HttpResponseMessage?)> ConnectAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { // If a non-infinite connect timeout has been set, create and use a new CancellationToken that will be canceled // when either the original token is canceled or a connect timeout occurs. @@ -1180,9 +1250,9 @@ public ValueTask SendAsync(HttpRequestMessage request, bool Debug.Assert(connection != null); TransportContext? transportContext = null; - if (_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel) + if (IsSecure) { - SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(allowHttp2 ? _sslOptionsHttp2! : _sslOptionsHttp11!, request, async, connection.Stream, cancellationToken).ConfigureAwait(false); + SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(GetSslOptionsForRequest(request), request, async, connection.Stream, cancellationToken).ConfigureAwait(false); connection = Connection.FromStream(sslStream, leaveOpen: false, connection.ConnectionProperties, connection.LocalEndPoint, connection.RemoteEndPoint); transportContext = sslStream.TransportContext; } @@ -1226,7 +1296,7 @@ private ValueTask ConnectToTcpHostAsync(string host, int port, HttpR internal async ValueTask<(HttpConnection?, HttpResponseMessage?)> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { (Connection? connection, TransportContext? transportContext, HttpResponseMessage? failureResponse) = - await ConnectAsync(request, async, false, cancellationToken).ConfigureAwait(false); + await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false); if (failureResponse != null) { @@ -1236,6 +1306,23 @@ private ValueTask ConnectToTcpHostAsync(string host, int port, HttpR return (ConstructHttp11Connection(connection!, transportContext), null); } + private SslClientAuthenticationOptions GetSslOptionsForRequest(HttpRequestMessage request) + { + if (_http2Enabled) + { + if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + return _sslOptionsHttp2Only!; + } + + if (request.Version.Major >= 2 || request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher) + { + return _sslOptionsHttp2!; + } + } + return _sslOptionsHttp11!; + } + private HttpConnection ConstructHttp11Connection(Connection connection, TransportContext? transportContext) { return _maxConnections == int.MaxValue ? 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 67906139abd503..1bd6f890614faa 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 @@ -252,7 +252,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox } else { - // No explicit Host header. Use host from uri. + // No explicit Host header. Use host from uri. sslHostName = uri.IdnHost; } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs index 42b67aabc31ac3..e3231b1c820aac 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs @@ -14,8 +14,6 @@ internal sealed class HttpConnectionSettings { private const string Http2SupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2SUPPORT"; private const string Http2SupportAppCtxSettingName = "System.Net.Http.SocketsHttpHandler.Http2Support"; - private const string Http2UnencryptedSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT"; - private const string Http2UnencryptedSupportAppCtxSettingName = "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport"; private const string Http3DraftSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT"; private const string Http3DraftSupportAppCtxSettingName = "System.Net.SocketsHttpHandler.Http3DraftSupport"; @@ -51,11 +49,6 @@ internal sealed class HttpConnectionSettings internal Version _maxHttpVersion; - internal bool _allowUnencryptedHttp2; - - // Used for testing until https://github.com/dotnet/runtime/issues/987 - internal bool _assumePrenegotiatedHttp3ForTesting; - internal SslClientAuthenticationOptions? _sslOptions; internal bool _enableMultipleHttp2Connections; @@ -72,7 +65,6 @@ public HttpConnectionSettings() AllowDraftHttp3 && allowHttp2 ? Http3Connection.HttpVersion30 : allowHttp2 ? HttpVersion.Version20 : HttpVersion.Version11; - _allowUnencryptedHttp2 = allowHttp2 && AllowUnencryptedHttp2; _defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials); _defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials; } @@ -111,8 +103,6 @@ public HttpConnectionSettings CloneAndNormalize() _sslOptions = _sslOptions?.ShallowClone(), // shallow clone the options for basic prevention of mutation issues while processing _useCookies = _useCookies, _useProxy = _useProxy, - _allowUnencryptedHttp2 = _allowUnencryptedHttp2, - _assumePrenegotiatedHttp3ForTesting = _assumePrenegotiatedHttp3ForTesting, _requestHeaderEncodingSelector = _requestHeaderEncodingSelector, _responseHeaderEncodingSelector = _responseHeaderEncodingSelector, _enableMultipleHttp2Connections = _enableMultipleHttp2Connections, @@ -147,32 +137,6 @@ private static bool AllowHttp2 } } - private static bool AllowUnencryptedHttp2 - { - get - { - // Default to not allowing unencrypted HTTP/2, but enable that to be overridden - // by an AppContext switch, or by an environment variable being to to true/1. - - // First check for the AppContext switch, giving it priority over the environment variable. - if (AppContext.TryGetSwitch(Http2UnencryptedSupportAppCtxSettingName, out bool allowHttp2)) - { - return allowHttp2; - } - - // AppContext switch wasn't used. Check the environment variable. - string? envVar = Environment.GetEnvironmentVariable(Http2UnencryptedSupportEnvironmentVariableSettingName); - if (envVar != null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1"))) - { - // Allow HTTP/2.0 protocol for HTTP endpoints. - return true; - } - - // Default to a maximum of HTTP/1.1. - return false; - } - } - private static bool AllowDraftHttp3 { get diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs index 73c1c3493af040..6a8a3d5f03921a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs @@ -422,6 +422,12 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request, throw new NotSupportedException(SR.Format(SR.net_http_http2_sync_not_supported, GetType())); } + // Do not allow upgrades for synchronous requests, that might lead to asynchronous code-paths. + if (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher) + { + throw new NotSupportedException(SR.Format(SR.net_http_upgrade_not_enabled_sync, nameof(Send), request.VersionPolicy)); + } + CheckDisposed(); HttpMessageHandlerStage handler = _handler ?? SetupHandlerChain(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.AltSvc.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.AltSvc.cs index 7fcd9372198dde..8986911aa0bed9 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.AltSvc.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.AltSvc.cs @@ -20,16 +20,7 @@ public HttpClientHandler_AltSvc_Test(ITestOutputHelper output) : base(output) /// protected override HttpClient CreateHttpClient() { - bool http3Enabled = (bool)typeof(SocketsHttpHandler) - .Assembly - .GetType("System.Net.Http.HttpConnectionSettings", throwOnError: true) - .GetProperty("AllowDraftHttp3", Reflection.BindingFlags.Static | Reflection.BindingFlags.NonPublic) - .GetValue(null); - - Assert.True(http3Enabled, "HTTP/3 draft support must be enabled for this test."); - HttpClientHandler handler = CreateHttpClientHandler(HttpVersion30); - SetUsePrenegotiatedHttp3(handler, usePrenegotiatedHttp3: false); return CreateHttpClient(handler); } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index defa4719d64fd7..2cfcd591505239 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -1873,13 +1873,13 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async url => using (var handler = new SocketsHttpHandler()) using (HttpClient client = CreateHttpClient(handler)) { - TestHelper.EnableUnencryptedHttp2IfNecessary(handler); handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; // Increase default Expect: 100-continue timeout to ensure that we don't accidentally fire the timer and send the request body. handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(300); var request = new HttpRequestMessage(HttpMethod.Post, url); request.Version = new Version(2,0); + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; request.Content = new StringContent(new string('*', 3000)); request.Headers.ExpectContinue = true; request.Headers.Add("x-test", "PostAsyncExpect100Continue_NonSuccessResponse_RequestBodyNotSent"); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs index bc718b29d66787..d0f9d5fe317366 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs @@ -3,6 +3,7 @@ using System.IO; using System.Reflection; +using System.Threading.Tasks; namespace System.Net.Http.Functional.Tests { @@ -14,36 +15,57 @@ protected static HttpClientHandler CreateHttpClientHandler(Version useVersion = { useVersion ??= HttpVersion.Version11; - HttpClientHandler handler = new HttpClientHandler(); + HttpClientHandler handler = PlatformDetection.SupportsAlpn ? new HttpClientHandler() : new VersionHttpClientHandler(useVersion); if (useVersion >= HttpVersion.Version20) { - TestHelper.EnableUnencryptedHttp2IfNecessary(handler); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; } + return handler; + } - if (useVersion == HttpVersion30) + protected static object GetUnderlyingSocketsHttpHandler(HttpClientHandler handler) + { + FieldInfo field = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic); + return field?.GetValue(handler); + } + + protected static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, Version version, bool exactVersion = false) => + new HttpRequestMessage(method, uri) { - SetUsePrenegotiatedHttp3(handler, usePrenegotiatedHttp3: true); - } + Version = version, + VersionPolicy = exactVersion ? HttpVersionPolicy.RequestVersionExact : HttpVersionPolicy.RequestVersionOrLower + }; + } - return handler; + internal class VersionHttpClientHandler : HttpClientHandler + { + private readonly Version _useVersion; + + public VersionHttpClientHandler(Version useVersion) + { + _useVersion = useVersion; } - /// - /// Used to bypass Alt-Svc until https://github.com/dotnet/runtime/issues/987 - /// - protected static void SetUsePrenegotiatedHttp3(HttpClientHandler handler, bool usePrenegotiatedHttp3) + protected override HttpResponseMessage Send(HttpRequestMessage request, Threading.CancellationToken cancellationToken) { - object socketsHttpHandler = GetUnderlyingSocketsHttpHandler(handler); - object settings = socketsHttpHandler.GetType().GetField("_settings", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(socketsHttpHandler); - settings.GetType().GetField("_assumePrenegotiatedHttp3ForTesting", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(settings, usePrenegotiatedHttp3); + if (request.Version == _useVersion) + { + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + } + + return base.Send(request, cancellationToken); } - protected static object GetUnderlyingSocketsHttpHandler(HttpClientHandler handler) + protected override Task SendAsync(HttpRequestMessage request, Threading.CancellationToken cancellationToken) { - FieldInfo field = typeof(HttpClientHandler).GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic); - return field?.GetValue(handler); + + if (request.Version == _useVersion) + { + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + } + + return base.SendAsync(request, cancellationToken); } protected static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, Version version, bool exactVersion = false) => diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs index 0074a06bc65725..840dd74ad7023e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs @@ -6,6 +6,8 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Quic; +using System.Net.Security; using System.Net.Sockets; using System.Net.Test.Common; using System.Text; @@ -13,6 +15,7 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; +using static System.Net.Test.Common.Configuration.Http; namespace System.Net.Http.Functional.Tests { @@ -901,7 +904,7 @@ await LoopbackServer.CreateClientAndServerAsync( Assert.IsType(ex.InnerException); }, async server => - { + { await server.AcceptConnectionAsync(async connection => { try @@ -1002,6 +1005,145 @@ await server.AcceptConnectionAsync(async connection => }); } + public static IEnumerable VersionSelectionMemberData() + { + var serverOptions = new GenericLoopbackOptions(); + // Either we support SSL (ALPN), or we're testing only clear text. + foreach (var useSsl in BoolValues.Where(b => serverOptions.UseSsl || !b)) + { + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version11 : typeof(HttpRequestException) }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version11 : typeof(HttpRequestException) }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version20 : typeof(HttpRequestException) }; + if (QuicConnection.IsQuicSupported) + { + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version11, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 }; + } + + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? (object)HttpVersion.Version20 : typeof(HttpRequestException) }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, HttpVersion.Version20 }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, HttpVersion.Version20 }; + if (QuicConnection.IsQuicSupported) + { + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, useSsl ? HttpVersion.Version20 : HttpVersion.Version11 }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, HttpVersion.Version20 }; + yield return new object[] { HttpVersion.Version20, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) }; + } + + if (QuicConnection.IsQuicSupported) + { + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version11, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version11, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version11, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion.Version20, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion.Version20, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion.Version20, useSsl, typeof(HttpRequestException) }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrLower, HttpVersion30, useSsl, useSsl ? HttpVersion30 : HttpVersion.Version11 }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionExact, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) }; + yield return new object[] { HttpVersion30, HttpVersionPolicy.RequestVersionOrHigher, HttpVersion30, useSsl, useSsl ? (object)HttpVersion30 : typeof(HttpRequestException) }; + } + } + } + + [Theory] + [MemberData(nameof(VersionSelectionMemberData))] + public async Task SendAsync_CorrectVersionSelected_LoopbackServer(Version requestVersion, HttpVersionPolicy versionPolicy, Version serverVersion, bool useSsl, object expectedResult) + { + await HttpAgnosticLoopbackServer.CreateClientAndServerAsync( + async uri => + { + var request = new HttpRequestMessage(HttpMethod.Get, uri) + { + Version = requestVersion, + VersionPolicy = versionPolicy + }; + + using HttpClientHandler handler = CreateHttpClientHandler(); + if (useSsl) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + using HttpClient client = CreateHttpClient(handler); + if (expectedResult is Type type) + { + Exception exception = await Assert.ThrowsAnyAsync(() => client.SendAsync(request)); + Assert.IsType(type, exception); + _output.WriteLine("Client expected exception: " + exception.ToString()); + } + else + { + HttpResponseMessage response = await client.SendAsync(request); + Assert.Equal(expectedResult, response.Version); + } + }, + async server => + { + try + { + HttpRequestData requestData = await server.AcceptConnectionSendResponseAndCloseAsync(); + Assert.Equal(expectedResult, requestData.Version); + } + catch (Exception ex) when (expectedResult is Type) + { + _output.WriteLine("Server exception: " + ex.ToString()); + } + }, httpOptions: new HttpAgnosticOptions() + { + UseSsl = useSsl, + ClearTextVersion = serverVersion, + SslApplicationProtocols = serverVersion.Major >= 2 ? new List{ SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 } : null + }); + } + + [OuterLoop("Uses external server")] + [Theory] + [MemberData(nameof(VersionSelectionMemberData))] + public async Task SendAsync_CorrectVersionSelected_ExternalServer(Version requestVersion, HttpVersionPolicy versionPolicy, Version serverVersion, bool useSsl, object expectedResult) + { + RemoteServer remoteServer = null; + if (serverVersion == HttpVersion.Version11) + { + remoteServer = useSsl ? RemoteSecureHttp11Server : RemoteHttp11Server; + } + if (serverVersion == HttpVersion.Version20) + { + remoteServer = useSsl ? RemoteHttp2Server : null; + } + // No remote server that could serve the requested version. + if (remoteServer == null) + { + _output.WriteLine($"Skipping test: No remote server that could serve the requested version."); + return; + } + + + var request = new HttpRequestMessage(HttpMethod.Get, remoteServer.EchoUri) + { + Version = requestVersion, + VersionPolicy = versionPolicy + }; + + using HttpClient client = CreateHttpClient(); + if (expectedResult is Type type) + { + Exception exception = await Assert.ThrowsAnyAsync(() => client.SendAsync(request)); + Assert.IsType(type, exception); + _output.WriteLine(exception.ToString()); + } + else + { + HttpResponseMessage response = await client.SendAsync(request); + Assert.Equal(expectedResult, response.Version); + } + } + [Fact] public void DefaultRequestVersion_InitialValueExpected() { diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 19de137c6434a3..5ff4d22ac3c674 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -218,6 +218,8 @@ Link="ProductionCode\System\Net\Http\HttpRuleParser.cs" /> +