HTTP Version Selection (#39201)
authorMarie Píchová <11718369+ManickaP@users.noreply.github.com>
Wed, 12 Aug 2020 08:26:34 +0000 (10:26 +0200)
committerGitHub <noreply@github.com>
Wed, 12 Aug 2020 08:26:34 +0000 (10:26 +0200)
25 files changed:
src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs
src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs
src/libraries/Common/tests/System/Net/Http/HttpAgnosticLoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs
src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/TestHelper.cs
src/libraries/System.Net.Http/ref/System.Net.Http.cs
src/libraries/System.Net.Http/src/Resources/Strings.resx
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpVersionPolicy.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.AltSvc.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.SocketsHttpHandler.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj

index da6e915..f4f92c0 100644 (file)
@@ -130,6 +130,7 @@ namespace System.Net.Test.Common
         public byte[] Body;
         public string Method;
         public string Path;
+        public Version Version;
         public List<HttpHeaderData> Headers { get; }
         public int RequestId;       // Generic request ID. Currently only used for HTTP/2 to hold StreamId.
 
@@ -143,6 +144,7 @@ namespace System.Net.Test.Common
             var result = new HttpRequestData();
             result.Method = request.Method.ToString();
             result.Path = request.RequestUri?.AbsolutePath;
+            result.Version = request.Version;
 
             foreach (var header in request.Headers)
             {
index acf18f5..846e2ac 100644 (file)
@@ -609,6 +609,7 @@ namespace System.Net.Test.Common
             // Extract method and path
             requestData.Method = requestData.GetSingleHeaderValue(":method");
             requestData.Path = requestData.GetSingleHeaderValue(":path");
+            requestData.Version = HttpVersion20.Value;
 
             if (readBody && !endOfStream)
             {
index b290c5a..06f8c46 100644 (file)
@@ -86,4 +86,9 @@ namespace System.Net.Test.Common
             throw new NotImplementedException("HTTP/3 does not operate over a Socket.");
         }
     }
+
+    public static class HttpVersion30
+    {
+        public static readonly Version Value = new Version(3, 0);
+    }
 }
index d0e8d43..3286aff 100644 (file)
@@ -201,6 +201,7 @@ namespace System.Net.Test.Common
                         break;
                 }
             }
+            request.Version = HttpVersion30.Value;
 
             return request;
         }
index 6a6657b..7e0f4e4 100644 (file)
@@ -54,77 +54,109 @@ namespace System.Net.Test.Common
                 _listenSocket = null;
             }
         }
+
         public override async Task<GenericLoopbackConnection> 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<SslApplicationProtocol>();
-                    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<byte>(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<byte>(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<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
@@ -162,12 +194,10 @@ namespace System.Net.Test.Common
 
     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<SslApplicationProtocol> SslApplicationProtocols { get; set; }
     }
 
     public sealed class HttpAgnosticLoopbackServerFactory : LoopbackServerFactory
@@ -259,5 +289,9 @@ namespace System.Net.Test.Common
         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();
+        }
     }
 }
index 371c47f..e889a86 100644 (file)
@@ -266,6 +266,7 @@ namespace System.Net.Http.Functional.Tests
             using HttpClient client = CreateHttpClient(handler);
 
             var options = new GenericLoopbackOptions { Address = address };
+
             await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
             {
                 _output.WriteLine(url.ToString());
index e9c7aa1..47e1ec2 100644 (file)
@@ -718,6 +718,7 @@ namespace System.Net.Test.Common
                 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
index 9cca3a6..041d6ef 100644 (file)
@@ -112,54 +112,6 @@ namespace System.Net.Http.Functional.Tests
                 .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];
index 415a52f..dbf1278 100644 (file)
@@ -49,6 +49,7 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
         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() { }
index 0ac22a4..61b45be 100644 (file)
   <data name="net_http_http2_sync_not_supported" xml:space="preserve">
     <value>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.</value>
   </data>
+  <data name="net_http_upgrade_not_enabled_sync" xml:space="preserve">
+    <value>HTTP request version upgrade is not enabled for synchronous '{0}'. Do not use '{1}' version policy for synchronous HTTP methods.</value>
+  </data>
+  <data name="net_http_requested_version_not_enabled" xml:space="preserve">
+    <value>Requesting HTTP version {0} with version policy {1} while HTTP/{2} is not enabled.</value>
+  </data>
+  <data name="net_http_requested_version_cannot_establish" xml:space="preserve">
+    <value>Requesting HTTP version {0} with version policy {1} while unable to establish HTTP/{2} connection.</value>
+  </data>
+  <data name="net_http_requested_version_alpn_refused" xml:space="preserve">
+    <value>Requesting HTTP version {0} with version policy {1} while server returned HTTP/1.1 in ALPN.</value>
+  </data>
+  <data name="net_http_requested_version_server_refused" xml:space="preserve">
+    <value>Requesting HTTP version {0} with version policy {1} while server offers only version fallback.</value>
+  </data>
 </root>
index 9e69e11..f7d8e23 100644 (file)
@@ -52,6 +52,7 @@
     <Compile Include="System\Net\Http\HttpRuleParser.cs" />
     <Compile Include="System\Net\Http\HttpTelemetry.cs" />
     <Compile Include="System\Net\Http\HttpUtilities.cs" />
+    <Compile Include="System\Net\Http\HttpVersionPolicy.cs" />
     <Compile Include="System\Net\Http\MessageProcessingHandler.cs" />
     <Compile Include="System\Net\Http\MultipartContent.cs" />
     <Compile Include="System\Net\Http\MultipartFormDataContent.cs" />
index a08edaf..dc3172c 100644 (file)
@@ -26,6 +26,7 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
             }
         }
 
+        /// <summary>
+        /// Gets or sets the default value of <see cref="HttpRequestMessage.VersionPolicy" /> for implicitly created requests in convenience methods,
+        /// e.g.: <see cref="GetAsync(string?)" />, <see cref="PostAsync(string?, HttpContent)" />.
+        /// </summary>
+        /// <remarks>
+        /// Note that this property has no effect on any of the <see cref="Send(HttpRequestMessage)" /> and <see cref="SendAsync(HttpRequestMessage)" /> overloads
+        /// since they accept fully initialized <see cref="HttpRequestMessage" />.
+        /// </remarks>
+        public HttpVersionPolicy DefaultVersionPolicy
+        {
+            get => _defaultVersionPolicy;
+            set
+            {
+                CheckDisposedOrStarted();
+                _defaultVersionPolicy = value;
+            }
+        }
+
         public Uri? BaseAddress
         {
             get { return _baseAddress; }
@@ -803,7 +822,7 @@ namespace System.Net.Http
             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
     }
 }
index f74fd02..5f9d7b6 100644 (file)
@@ -24,6 +24,7 @@ namespace System.Net.Http
         private Uri? _requestUri;
         private HttpRequestHeaders? _headers;
         private Version _version;
+        private HttpVersionPolicy _versionPolicy;
         private HttpContent? _content;
         private bool _disposed;
         private HttpRequestOptions? _options;
@@ -43,6 +44,20 @@ namespace System.Net.Http
             }
         }
 
+        /// <summary>
+        /// Gets or sets the policy determining how <see cref="Version" /> is interpreted and how is the final HTTP version negotiated with the server.
+        /// </summary>
+        public HttpVersionPolicy VersionPolicy
+        {
+            get { return _versionPolicy; }
+            set
+            {
+                CheckDisposed();
+
+                _versionPolicy = value;
+            }
+        }
+
         public HttpContent? Content
         {
             get { return _content; }
@@ -181,6 +196,7 @@ namespace System.Net.Http
             _method = method;
             _requestUri = requestUri;
             _version = HttpUtilities.DefaultRequestVersion;
+            _versionPolicy = HttpUtilities.DefaultVersionPolicy;
         }
 
         internal bool MarkAsSent()
index 189a250..53edcaa 100644 (file)
@@ -13,6 +13,8 @@ namespace System.Net.Http
 
         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 (file)
index 0000000..9f3ddb5
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Determines behavior when selecting and negotiating HTTP version for a request.
+    /// </summary>
+    public enum HttpVersionPolicy
+    {
+        /// <summary>
+        /// Default behavior, either uses requested version or downgrades to a lower one.
+        /// </summary>
+        /// <remarks>
+        /// 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 <see cref="HttpRequestMessage.Version" />.
+        /// Otherwise, downgrades to HTTP/1.1.
+        /// Note that this option does not allow use of prenegotiated clear text connection, e.g. H2C.
+        /// </remarks>
+        RequestVersionOrLower,
+
+        /// <summary>
+        /// Tries to uses highest available version, downgrading only to the requested version, not bellow.
+        /// Throwing <see cref="HttpRequestException" /> if a connection with higher or equal version cannot be established.
+        /// </summary>
+        /// <remarks>
+        /// 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 <see cref="HttpRequestMessage.Version" />.
+        /// Note that this option allows to use prenegotiated clear text connection for the requested version but not for anything higher.
+        /// </remarks>
+        RequestVersionOrHigher,
+
+        /// <summary>
+        /// Uses only the requested version.
+        /// Throwing <see cref="HttpRequestException" /> if a connection with the exact version cannot be established.
+        /// </summary>
+        /// <remarks>
+        /// Note that this option allows to use prenegotiated clear text connection for the requested version.
+        /// </remarks>
+        RequestVersionExact
+    }
+}
index df8191b..36a5714 100644 (file)
@@ -73,16 +73,6 @@ namespace System.Net.Http
             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<byte> value)
index e6644c1..c74f32a 100644 (file)
@@ -83,6 +83,7 @@ namespace System.Net.Http
         /// <summary>Options specialized and cached for this pool and its key.</summary>
         private readonly SslClientAuthenticationOptions? _sslOptionsHttp11;
         private readonly SslClientAuthenticationOptions? _sslOptionsHttp2;
+        private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only;
         private readonly SslClientAuthenticationOptions? _sslOptionsHttp3;
 
         /// <summary>Queue of waiters waiting for a connection.  Created on demand.</summary>
@@ -116,11 +117,6 @@ namespace System.Net.Http
             if (host != null)
             {
                 _originAuthority = new HttpAuthority(host, port);
-
-                if (_poolManager.Settings._assumePrenegotiatedHttp3ForTesting)
-                {
-                    _http3Authority = _originAuthority;
-                }
             }
 
             _http2Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version20;
@@ -133,7 +129,7 @@ namespace System.Net.Http
                     Debug.Assert(port != 0);
                     Debug.Assert(sslHostName == null);
                     Debug.Assert(proxyUri == null);
-                    _http2Enabled = _poolManager.Settings._allowUnencryptedHttp2;
+
                     _http3Enabled = false;
                     break;
 
@@ -169,6 +165,7 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
                 {
                     _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 @@ namespace System.Net.Http
                     Debug.Assert(hostHeader != null);
                     _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader);
                     _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader);
-
                 }
 
                 if (_http3Enabled)
@@ -261,6 +259,7 @@ namespace System.Net.Http
 
         private static readonly List<SslApplicationProtocol> s_http3ApplicationProtocols = new List<SslApplicationProtocol>() { Http3Connection.Http3ApplicationProtocol };
         private static readonly List<SslApplicationProtocol> s_http2ApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 };
+        private static readonly List<SslApplicationProtocol> s_http2OnlyApplicationProtocols = new List<SslApplicationProtocol>() { SslApplicationProtocol.Http2 };
 
         private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnectionPoolManager poolManager, string sslHostName)
         {
@@ -290,8 +289,8 @@ namespace System.Net.Http
 
         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 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
                 {
                     _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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
                 }
                 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 @@ namespace System.Net.Http
                     {
                         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;
@@ -994,6 +1049,22 @@ namespace System.Net.Http
         }
 
         /// <summary>
+        /// Checks whether the given <paramref name="authority"/> is on the currext Alt-Svc blocklist.
+        /// </summary>
+        /// <seealso cref="BlocklistAuthority" />
+        private bool IsAltSvcBlocked(HttpAuthority authority)
+        {
+            if (_altSvcBlocklist != null)
+            {
+                lock (_altSvcBlocklist)
+                {
+                    return _altSvcBlocklist.Contains(authority);
+                }
+            }
+            return false;
+        }
+
+        /// <summary>
         /// Blocklists an authority and resets the current authority back to origin.
         /// If the number of blocklisted authorities exceeds <see cref="MaxAltSvcIgnoreListSize"/>,
         /// Alt-Svc will be disabled entirely for a period of time.
@@ -1009,7 +1080,6 @@ namespace System.Net.Http
         internal void BlocklistAuthority(HttpAuthority badAuthority)
         {
             Debug.Assert(badAuthority != null);
-            Debug.Assert(badAuthority != _originAuthority);
 
             HashSet<HttpAuthority>? altSvcBlocklist = _altSvcBlocklist;
 
@@ -1136,7 +1206,7 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
             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 new HttpConnection(this, connection, transportContext);
index 06575e4..601b583 100644 (file)
@@ -252,7 +252,7 @@ namespace System.Net.Http
                 }
                 else
                 {
-                    // No explicit Host header.  Use host from uri.
+                    // No explicit Host header. Use host from uri.
                     sslHostName = uri.IdnHost;
                 }
             }
index 42b67aa..e3231b1 100644 (file)
@@ -14,8 +14,6 @@ namespace System.Net.Http
     {
         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 @@ namespace System.Net.Http
 
         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 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                 _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 @@ namespace System.Net.Http
             }
         }
 
-        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
index 73c1c34..6a8a3d5 100644 (file)
@@ -422,6 +422,12 @@ namespace System.Net.Http
                 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();
 
index 7fcd937..8986911 100644 (file)
@@ -20,16 +20,7 @@ namespace System.Net.Http.Functional.Tests
         /// </summary>
         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);
         }
index 9ef63d4..28cae67 100644 (file)
@@ -1873,13 +1873,13 @@ namespace System.Net.Http.Functional.Tests
                 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");
index bc718b2..d0f9d5f 100644 (file)
@@ -3,6 +3,7 @@
 
 using System.IO;
 using System.Reflection;
+using System.Threading.Tasks;
 
 namespace System.Net.Http.Functional.Tests
 {
@@ -14,36 +15,57 @@ namespace System.Net.Http.Functional.Tests
         {
             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;
         }
 
-        /// <summary>
-        /// Used to bypass Alt-Svc until https://github.com/dotnet/runtime/issues/987
-        /// </summary>
-        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<HttpResponseMessage> 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) =>
index 0074a06..840dd74 100644 (file)
@@ -6,6 +6,8 @@ using System.Collections.Generic;
 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;
 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 @@ namespace System.Net.Http.Functional.Tests
                     Assert.IsType<TimeoutException>(ex.InnerException);
                 },
                 async server =>
-                { 
+                {
                     await server.AcceptConnectionAsync(async connection =>
                     {
                         try
@@ -1002,6 +1005,145 @@ namespace System.Net.Http.Functional.Tests
                 }); 
         }
 
+        public static IEnumerable<object[]> 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<Exception>(() => 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>{ 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<Exception>(() => 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()
         {
index 6bf7e93..ea67ef0 100644 (file)
              Link="ProductionCode\System\Net\Http\HttpRuleParser.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpUtilities.cs"
              Link="ProductionCode\System\Net\Http\HttpUtilities.cs" />
+    <Compile Include="..\..\src\System\Net\Http\HttpVersionPolicy.cs"
+             Link="ProductionCode\System\Net\Http\HttpVersionPolicy.cs" />
     <Compile Include="..\..\src\System\Net\Http\MessageProcessingHandler.cs"
              Link="ProductionCode\System\Net\Http\MessageProcessingHandler.cs" />
     <Compile Include="..\..\src\System\Net\Http\MultipartContent.cs"