Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f31c191
VersionPolicy API added.
ManickaP Jul 9, 2020
4abfab1
WIP Version Policy.
ManickaP Jul 10, 2020
f3fcff0
Cleared up some ToDos.
ManickaP Jul 14, 2020
83b8844
Some more version upgrade/downgrade logic.
ManickaP Jul 15, 2020
04a37a1
Enforce H2C in tests when ALPN is not available.
ManickaP Jul 17, 2020
08f085b
Exception messages into resources.
ManickaP Jul 19, 2020
ce5f21e
Some fixes.
ManickaP Jul 19, 2020
53a08d1
Loopback tests.
ManickaP Jul 21, 2020
959f85f
RemoteServer tests and fixes.
ManickaP Jul 21, 2020
c0a731f
H3 assumed prenegotiated only when explicitly requested.
ManickaP Jul 21, 2020
e332222
Netfx compilaris
ManickaP Jul 21, 2020
24df30f
Typo fixes
ManickaP Jul 22, 2020
d716c5a
ValueTask.FromException instead of throw
ManickaP Jul 22, 2020
0129cc1
Merge branch 'master' into mapichov/987_http_version_selection
ManickaP Jul 28, 2020
ef11ae9
Merge branch 'master' into mapichov/987_http_version_selection
ManickaP Jul 28, 2020
cf25acb
Blocklisting H3 authority after failed connection attempt.
ManickaP Jul 28, 2020
ea57adf
Fixed merge
ManickaP Jul 28, 2020
88343a1
H3 blocked alt-svc authority.
ManickaP Jul 28, 2020
f0b0676
Merge branch 'master' into mapichov/987_http_version_selection
ManickaP Jul 30, 2020
962530b
Merge branch 'master' into mapichov/987_http_version_selection
ManickaP Aug 3, 2020
2ed24f5
Adapted version selection tests to HttpAgnosticLoopbackServer
ManickaP Aug 5, 2020
884a710
Merge branch 'master' into mapichov/987_http_version_selection
ManickaP Aug 10, 2020
63f7b05
Addressed PR feedback.
ManickaP Aug 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public class HttpRequestData
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.

Expand All @@ -143,6 +144,7 @@ public static async Task<HttpRequestData> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ public async Task<byte[]> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ public override Task<GenericLoopbackConnection> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ private HttpRequestData ParseHeaders(ReadOnlySpan<byte> buffer)
break;
}
}
request.Version = HttpVersion30.Value;

return request;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,77 +54,109 @@ public override void Dispose()
_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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we construct this server with a bool unencryptedProtocolDetection = false and throw if !unencryptedProtocolDetection && !_options.UseSsl?

I am worried that we will hide test bugs by accidentally using HTTP/1 when HTTP/2 was wanted, etc. -- this behavior should be off by default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is enforced in ClearTextVersion in HttpAgnosticOptions which defaults to HttpVersion.Version11.
So unless you construct the server with ClearTextVersion == HttpVersion.Unknown it won't do any unencrypted protocol detection.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, I removed the default of HTTP/1.1 and I'm explicitly checking for null ClearTextVersion and throwing if not set.

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 = "")
Expand Down Expand Up @@ -162,12 +194,10 @@ public static async Task CreateClientAndServerAsync(Func<Uri, Task> 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<SslApplicationProtocol> SslApplicationProtocols { get; set; }
}

public sealed class HttpAgnosticLoopbackServerFactory : LoopbackServerFactory
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ public override async Task<HttpRequestData> 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
Expand Down
48 changes: 0 additions & 48 deletions src/libraries/Common/tests/System/Net/Http/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
8 changes: 8 additions & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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() { }
Expand Down
15 changes: 15 additions & 0 deletions src/libraries/System.Net.Http/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -567,4 +567,19 @@
<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>
1 change: 1 addition & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
21 changes: 20 additions & 1 deletion src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +58,24 @@ public Version DefaultRequestVersion
}
}

/// <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; }
Expand Down Expand Up @@ -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
}
}
Loading