support HTTP/2 without TLS (dotnet/corefx#36753)
authorTomas Weinfurt <tweinfurt@yahoo.com>
Tue, 16 Apr 2019 18:54:24 +0000 (11:54 -0700)
committerGitHub <noreply@github.com>
Tue, 16 Apr 2019 18:54:24 +0000 (11:54 -0700)
* support HTTP/2 without ssl

* feedback from review

* revert changes GetAsync->SendAsync

* remove unused variable

* feedback from review

* feedback from review

* add missong Trace().

Commit migrated from https://github.com/dotnet/corefx/commit/7ed0c5338941346a3cacb4e18f19aaddb283da90

src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/StaticTable.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.cs
src/libraries/System.Net.Http/tests/FunctionalTests/PlatformHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/TestHelper.cs

index 6c14210..f70024e 100644 (file)
@@ -665,7 +665,7 @@ namespace System.Net.Test.Common
     {
         public IPAddress Address { get; set; } = IPAddress.Loopback;
         public int ListenBacklog { get; set; } = 1;
-        public bool UseSsl { get; set; } = true;
+        public bool UseSsl { get; set; } = PlatformDetection.SupportsAlpn;
         public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12;
     }
 
index cf64ac7..ff0c45e 100644 (file)
@@ -86,11 +86,12 @@ namespace System.Net.Http.HPack
                 value.Length != 0 ? Encoding.ASCII.GetBytes(value) : Array.Empty<byte>());
 
         // Values for encoding.
-        // Unused values are omitted, so entries like ":scheme: http" are not included.
+        // Unused values are omitted.
         public const int Authority = 1;
         public const int MethodGet = 2;
         public const int MethodPost = 3;
         public const int PathSlash = 4;
+        public const int SchemeHttp = 6;
         public const int SchemeHttps = 7;
         public const int AcceptCharset = 15;
         public const int AcceptEncoding = 16;
index b8e46de..e338ef0 100644 (file)
@@ -19,7 +19,7 @@ namespace System.Net.Http
     internal sealed partial class Http2Connection : HttpConnectionBase, IDisposable
     {
         private readonly HttpConnectionPool _pool;
-        private readonly SslStream _stream;
+        private readonly Stream _stream;
 
         // NOTE: These are mutable structs; do not make these readonly.
         private ArrayBuffer _incomingBuffer;
@@ -78,7 +78,7 @@ namespace System.Net.Http
         // this value, so this is not a hard maximum size.
         private const int UnflushedOutgoingBufferSize = 32 * 1024;
 
-        public Http2Connection(HttpConnectionPool pool, SslStream stream)
+        public Http2Connection(HttpConnectionPool pool, Stream stream)
         {
             _pool = pool;
             _stream = stream;
@@ -899,7 +899,7 @@ namespace System.Net.Http
                 WriteIndexedHeader(StaticTable.MethodGet, normalizedMethod.Method);
             }
 
-            WriteIndexedHeader(StaticTable.SchemeHttps);
+            WriteIndexedHeader(_stream is SslStream ? StaticTable.SchemeHttps : StaticTable.SchemeHttp);
 
             if (request.HasHeaders && request.Headers.Host != null)
             {
index 3861882..5b95496 100644 (file)
@@ -28,7 +28,7 @@ namespace System.Net.Http
         private readonly int _port;
         private readonly Uri _proxyUri;
         internal readonly byte[] _encodedAuthorityHostHeader;
-        
+
         /// <summary>List of idle connections stored in the pool.</summary>
         private readonly List<CachedConnection> _idleConnections = new List<CachedConnection>();
         /// <summary>The maximum number of connections allowed to be associated with the pool.</summary>
@@ -77,8 +77,7 @@ namespace System.Net.Http
                     Debug.Assert(port != 0);
                     Debug.Assert(sslHostName == null);
                     Debug.Assert(proxyUri == null);
-
-                    _http2Enabled = false;
+                    _http2Enabled = _poolManager.Settings._allowUnencryptedHttp2;
                     break;
 
                 case HttpConnectionKind.Https:
@@ -140,6 +139,10 @@ namespace System.Net.Http
                 // Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
                 _hostHeaderValueBytes = Encoding.ASCII.GetBytes(hostHeader);
                 Debug.Assert(Encoding.ASCII.GetString(_hostHeaderValueBytes) == hostHeader);
+                if (sslHostName == null)
+                {
+                    _encodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(StaticTable.Authority, hostHeader);
+                }
             }
 
             if (sslHostName != null)
@@ -328,7 +331,7 @@ namespace System.Net.Http
         private async ValueTask<(HttpConnectionBase connection, bool isNewConnection, HttpResponseMessage failureResponse)>
             GetHttp2ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
         {
-            Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel);
+            Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http);
 
             // See if we have an HTTP2 connection
             Http2Connection http2Connection = _http2Connection;
@@ -401,6 +404,22 @@ namespace System.Net.Http
                         return (null, true, failureResponse);
                     }
 
+                    if (_kind == HttpConnectionKind.Http)
+                    {
+                        http2Connection = new Http2Connection(this, stream);
+                        await http2Connection.SetupAsync().ConfigureAwait(false);
+
+                        Debug.Assert(_http2Connection == null);
+                        _http2Connection = http2Connection;
+
+                        if (NetEventSource.IsEnabled)
+                        {
+                            Trace("New unencrypted HTTP2 connection established.");
+                        }
+
+                        return (_http2Connection, true, null);
+                    }
+
                     sslStream = (SslStream)stream;
                     if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
                     {
index 5e3f677..a8cb624 100644 (file)
@@ -12,7 +12,9 @@ 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";
+
         internal DecompressionMethods _automaticDecompression = HttpHandlerDefaults.DefaultAutomaticDecompression;
 
         internal bool _useCookies = HttpHandlerDefaults.DefaultUseCookies;
@@ -40,13 +42,17 @@ namespace System.Net.Http
 
         internal Version _maxHttpVersion;
 
+        internal bool _allowUnencryptedHttp2;
+
         internal SslClientAuthenticationOptions _sslOptions;
 
         internal IDictionary<string, object> _properties;
 
         public HttpConnectionSettings()
         {
-            _maxHttpVersion = AllowHttp2 ? HttpVersion.Version20 : HttpVersion.Version11;
+            bool allowHttp2 = AllowHttp2;
+            _maxHttpVersion = allowHttp2 ? HttpVersion.Version20 : HttpVersion.Version11;
+            _allowUnencryptedHttp2 = allowHttp2 && AllowUnencryptedHttp2;
         }
 
         /// <summary>Creates a copy of the settings but with some values normalized to suit the implementation.</summary>
@@ -92,6 +98,7 @@ 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,
             };
         }
 
@@ -117,5 +124,28 @@ namespace System.Net.Http
                 return false;
             }
         }
+
+        private static bool AllowUnencryptedHttp2
+        {
+            get
+            {
+                // 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;
+            }
+        }
     }
 }
index daba271..d25d7cc 100644 (file)
@@ -21,7 +21,7 @@ namespace System.Net.Http.Functional.Tests
 
         public HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output) { }
 
-        [ConditionalFact(nameof(SupportsAlpn))]
+        [Fact]
         public async Task Http2_ClientPreface_Sent()
         {
             using (var server = Http2LoopbackServer.CreateServer())
@@ -35,7 +35,7 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        [ConditionalFact(nameof(SupportsAlpn))]
+        [Fact]
         public async Task Http2_InitialSettings_SentAndAcked()
         {
             using (var server = Http2LoopbackServer.CreateServer())
@@ -67,7 +67,7 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        [ConditionalFact(nameof(SupportsAlpn))]
+        [Fact]
         public async Task Http2_DataSentBeforeServerPreface_ProtocolError()
         {
             using (var server = Http2LoopbackServer.CreateServer())
@@ -85,7 +85,7 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        [ConditionalFact(nameof(SupportsAlpn))]
+        [Fact]
         public async Task Http2_NoResponseBody_Success()
         {
             using (var server = Http2LoopbackServer.CreateServer())
index 129a933..922bfd7 100644 (file)
@@ -56,10 +56,9 @@ namespace System.Net.Http.Functional.Tests
                 Debug.Assert(useSocketsHttpHandler == IsSocketsHttpHandler(handler), "Unexpected handler.");
             }
 
-            TestHelper.EnsureHttp2Feature(handler);
-
             if (useHttp2LoopbackServer)
             {
+                TestHelper.EnsureHttp2Feature(handler);
                 handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
             }
 
index 85f0d2f..778af07 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Collections.Generic;
 using System.Net.Test.Common;
 using System.Threading.Tasks;
 using Xunit;
@@ -12,6 +13,8 @@ namespace System.Net.Http.Functional.Tests
 
     public class PlatformHandler_HttpClientHandler : HttpClientHandlerTestBase
     {
+        protected override bool UseSocketsHttpHandler => false;
+
         public PlatformHandler_HttpClientHandler(ITestOutputHelper output) : base(output) { }
 
         [Theory]
index 67417ab..80a7a30 100644 (file)
@@ -1712,7 +1712,7 @@ namespace System.Net.Http.Functional.Tests
         protected override bool UseSocketsHttpHandler => true;
         protected override bool UseHttp2LoopbackServer => true;
     }
-    
+
     [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
     public sealed class SocketsHttpHandler_HttpClientHandler_Cancellation_Test_Http2 : HttpClientHandler_Cancellation_Test
     {
index ed800d2..42a9abc 100644 (file)
@@ -155,6 +155,12 @@ namespace System.Net.Http.Functional.Tests
                 "_maxHttpVersion",
                 BindingFlags.NonPublic | BindingFlags.Instance);
             field_maxHttpVersion.SetValue(_settings, new Version(2, 0));
+
+            // Allow HTTP/2.0 via unencrypted socket if ALPN is not supported on platform.
+            FieldInfo field_allowPlainHttp2 = type_HttpConnectionSettings.GetField(
+                "_allowUnencryptedHttp2",
+                BindingFlags.NonPublic | BindingFlags.Instance);
+            field_allowPlainHttp2.SetValue(_settings, !PlatformDetection.SupportsAlpn);
         }
 
         public static bool NativeHandlerSupportsSslConfiguration()