From b6b2da9344adebf2bef3d72bf9983c9e297824c8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 16 Feb 2018 12:02:45 -0500 Subject: [PATCH] Improve test coverage of SocketsHttpHandler (dotnet/corefx#27135) * Improve test coverage of SocketsHttpHandler Used code coverage information to write targeted tests to cover various uncovered paths in SocketsHttpHandler. Along the way fixed a variety of issues relating to throwing the wrong exception type, a stack overflow due to an unexpected recursive call, etc. * Address PR feedback * Re-disable a test Commit migrated from https://github.com/dotnet/corefx/commit/39677219aaeab8c6591b625ab2874acb17eaf4ef --- .../Common/src/System/Net/HttpStatusDescription.cs | 15 +- .../tests/System/Net/Configuration.Certificates.cs | 2 +- .../ref/CoreFx.Private.TestUtilities.cs | 1 + .../System/Diagnostics/RemoteExecutorTestBase.cs | 14 + .../AnonymousPipeTest.CrossProcess.cs | 2 +- .../NamedPipeTests/NamedPipeTest.CrossProcess.cs | 4 +- .../NamedPipeTest.RunAsClient.Unix.cs | 4 +- .../src/System/Net/Http/ByteArrayHelpers.cs | 4 +- .../System/Net/Http/Headers/HttpHeaderParser.cs | 24 - .../ChunkedEncodingReadStream.cs | 30 +- .../ChunkedEncodingWriteStream.cs | 12 +- .../ConnectionCloseReadStream.cs | 43 +- .../SocketsHttpHandler/ContentLengthReadStream.cs | 31 +- .../SocketsHttpHandler/ContentLengthWriteStream.cs | 9 - .../SocketsHttpHandler/DecompressionHandler.cs | 10 +- .../Net/Http/SocketsHttpHandler/EmptyReadStream.cs | 10 +- .../Net/Http/SocketsHttpHandler/HttpConnection.cs | 20 +- .../SocketsHttpHandler/HttpContentDuplexStream.cs | 20 +- .../SocketsHttpHandler/HttpContentReadStream.cs | 12 +- .../Http/SocketsHttpHandler/HttpContentStream.cs | 29 +- .../SocketsHttpHandler/HttpContentWriteStream.cs | 36 +- .../HttpProxyConnectionHandler.cs | 2 +- .../Net/Http/SocketsHttpHandler/HttpSystemProxy.cs | 3 - .../Http/SocketsHttpHandler/RawConnectionStream.cs | 44 +- .../tests/FunctionalTests/DiagnosticsTests.cs | 15 +- .../HttpClientHandlerTest.ClientCertificates.cs | 59 ++- .../tests/FunctionalTests/HttpClientHandlerTest.cs | 572 ++++++++++++++++++++- .../tests/FunctionalTests/HttpClientTestBase.cs | 19 +- .../tests/FunctionalTests/HttpProtocolTests.cs | 7 +- .../FunctionalTests/SocketsHttpHandlerTest.cs | 217 +++++++- .../System.Threading/tests/SemaphoreTests.cs | 2 +- 31 files changed, 1029 insertions(+), 243 deletions(-) diff --git a/src/libraries/Common/src/System/Net/HttpStatusDescription.cs b/src/libraries/Common/src/System/Net/HttpStatusDescription.cs index a59baad..3bbc640 100644 --- a/src/libraries/Common/src/System/Net/HttpStatusDescription.cs +++ b/src/libraries/Common/src/System/Net/HttpStatusDescription.cs @@ -2,8 +2,6 @@ // 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.Runtime.CompilerServices; - namespace System.Net { internal static class HttpStatusDescription @@ -20,6 +18,7 @@ namespace System.Net case 100: return "Continue"; case 101: return "Switching Protocols"; case 102: return "Processing"; + case 103: return "Early Hints"; case 200: return "OK"; case 201: return "Created"; @@ -29,6 +28,8 @@ namespace System.Net case 205: return "Reset Content"; case 206: return "Partial Content"; case 207: return "Multi-Status"; + case 208: return "Already Reported"; + case 226: return "IM Used"; case 300: return "Multiple Choices"; case 301: return "Moved Permanently"; @@ -37,6 +38,7 @@ namespace System.Net case 304: return "Not Modified"; case 305: return "Use Proxy"; case 307: return "Temporary Redirect"; + case 308: return "Permanent Redirect"; case 400: return "Bad Request"; case 401: return "Unauthorized"; @@ -56,10 +58,15 @@ namespace System.Net case 415: return "Unsupported Media Type"; case 416: return "Requested Range Not Satisfiable"; case 417: return "Expectation Failed"; + case 421: return "Misdirected Request"; case 422: return "Unprocessable Entity"; case 423: return "Locked"; case 424: return "Failed Dependency"; case 426: return "Upgrade Required"; // RFC 2817 + case 428: return "Precondition Required"; + case 429: return "Too Many Requests"; + case 431: return "Request Header Fields Too Large"; + case 451: return "Unavailable For Legal Reasons"; case 500: return "Internal Server Error"; case 501: return "Not Implemented"; @@ -67,7 +74,11 @@ namespace System.Net case 503: return "Service Unavailable"; case 504: return "Gateway Timeout"; case 505: return "Http Version Not Supported"; + case 506: return "Variant Also Negotiates"; case 507: return "Insufficient Storage"; + case 508: return "Loop Detected"; + case 510: return "Not Extended"; + case 511: return "Network Authentication Required"; } return null; } diff --git a/src/libraries/Common/tests/System/Net/Configuration.Certificates.cs b/src/libraries/Common/tests/System/Net/Configuration.Certificates.cs index b8793be..85cd06e 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.Certificates.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.Certificates.cs @@ -57,7 +57,7 @@ namespace System.Net.Test.Common private static X509Certificate2Collection GetCertificateCollection(string certificateFileName) { - // On Windows, .Net Core applications should not import PFX files in parallel to avoid a known system-level race condition. + // On Windows, .NET Core applications should not import PFX files in parallel to avoid a known system-level race condition. // This bug results in corrupting the X509Certificate2 certificate state. try { diff --git a/src/libraries/CoreFx.Private.TestUtilities/ref/CoreFx.Private.TestUtilities.cs b/src/libraries/CoreFx.Private.TestUtilities/ref/CoreFx.Private.TestUtilities.cs index cd4450a..b6d03a5 100644 --- a/src/libraries/CoreFx.Private.TestUtilities/ref/CoreFx.Private.TestUtilities.cs +++ b/src/libraries/CoreFx.Private.TestUtilities/ref/CoreFx.Private.TestUtilities.cs @@ -140,6 +140,7 @@ namespace System.Diagnostics public static System.Diagnostics.RemoteExecutorTestBase.RemoteInvokeHandle RemoteInvoke(System.Func method, string arg1, string arg2, string arg3, string arg4, string arg5, System.Diagnostics.RemoteInvokeOptions options = null) { throw null; } public static System.Diagnostics.RemoteExecutorTestBase.RemoteInvokeHandle RemoteInvoke(System.Func> method, System.Diagnostics.RemoteInvokeOptions options = null) { throw null; } public static System.Diagnostics.RemoteExecutorTestBase.RemoteInvokeHandle RemoteInvoke(System.Func> method, string arg, System.Diagnostics.RemoteInvokeOptions options = null) { throw null; } + public static System.Diagnostics.RemoteExecutorTestBase.RemoteInvokeHandle RemoteInvoke(System.Func> method, string arg1, string arg2, System.Diagnostics.RemoteInvokeOptions options = null) { throw null; } public static System.Diagnostics.RemoteExecutorTestBase.RemoteInvokeHandle RemoteInvokeRaw(System.Delegate method, string unparsedArg, System.Diagnostics.RemoteInvokeOptions options = null) { throw null; } public sealed partial class RemoteInvokeHandle : System.IDisposable { diff --git a/src/libraries/CoreFx.Private.TestUtilities/src/System/Diagnostics/RemoteExecutorTestBase.cs b/src/libraries/CoreFx.Private.TestUtilities/src/System/Diagnostics/RemoteExecutorTestBase.cs index f06fe27..dd97cb1 100644 --- a/src/libraries/CoreFx.Private.TestUtilities/src/System/Diagnostics/RemoteExecutorTestBase.cs +++ b/src/libraries/CoreFx.Private.TestUtilities/src/System/Diagnostics/RemoteExecutorTestBase.cs @@ -58,6 +58,7 @@ namespace System.Diagnostics /// Invokes the method from this assembly in another process using the specified arguments. /// The method to invoke. + /// The argument to pass to the method. /// Options to use for the invocation. public static RemoteInvokeHandle RemoteInvoke( Func> method, @@ -70,6 +71,19 @@ namespace System.Diagnostics /// Invokes the method from this assembly in another process using the specified arguments. /// The method to invoke. /// The first argument to pass to the method. + /// The second argument to pass to the method. + /// Options to use for the invocation. + public static RemoteInvokeHandle RemoteInvoke( + Func> method, + string arg1, string arg2, + RemoteInvokeOptions options = null) + { + return RemoteInvoke(GetMethodInfo(method), new[] { arg1, arg2 }, options); + } + + /// Invokes the method from this assembly in another process using the specified arguments. + /// The method to invoke. + /// The argument to pass to the method. /// Options to use for the invocation. public static RemoteInvokeHandle RemoteInvoke( Func method, diff --git a/src/libraries/System.IO.Pipes/tests/AnonymousPipeTests/AnonymousPipeTest.CrossProcess.cs b/src/libraries/System.IO.Pipes/tests/AnonymousPipeTests/AnonymousPipeTest.CrossProcess.cs index c07b10c..a57347c 100644 --- a/src/libraries/System.IO.Pipes/tests/AnonymousPipeTests/AnonymousPipeTest.CrossProcess.cs +++ b/src/libraries/System.IO.Pipes/tests/AnonymousPipeTests/AnonymousPipeTest.CrossProcess.cs @@ -17,7 +17,7 @@ namespace System.IO.Pipes.Tests // Then spawn another process to communicate with. using (var outbound = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) using (var inbound = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable)) - using (var remote = RemoteInvoke(PingPong_OtherProcess, outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString())) + using (var remote = RemoteInvoke(new Func(PingPong_OtherProcess), outbound.GetClientHandleAsString(), inbound.GetClientHandleAsString())) { // Close our local copies of the handles now that we've passed them of to the other process outbound.DisposeLocalCopyOfClientHandle(); diff --git a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs index 482d5f5..bb3455b 100644 --- a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs +++ b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs @@ -22,7 +22,7 @@ namespace System.IO.Pipes.Tests // another process with which to communicate using (var outbound = new NamedPipeServerStream(outName, PipeDirection.Out)) using (var inbound = new NamedPipeClientStream(".", inName, PipeDirection.In)) - using (RemoteInvoke(PingPong_OtherProcess, outName, inName)) + using (RemoteInvoke(new Func(PingPong_OtherProcess), outName, inName)) { // Wait for both pipes to be connected Task.WaitAll(outbound.WaitForConnectionAsync(), inbound.ConnectAsync()); @@ -48,7 +48,7 @@ namespace System.IO.Pipes.Tests // another process with which to communicate using (var outbound = new NamedPipeServerStream(outName, PipeDirection.Out)) using (var inbound = new NamedPipeClientStream(".", inName, PipeDirection.In)) - using (RemoteInvoke(PingPong_OtherProcess, outName, inName)) + using (RemoteInvoke(new Func(PingPong_OtherProcess), outName, inName)) { // Wait for both pipes to be connected await Task.WhenAll(outbound.WaitForConnectionAsync(), inbound.ConnectAsync()); diff --git a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.RunAsClient.Unix.cs b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.RunAsClient.Unix.cs index e998490..fd3e9b6 100644 --- a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.RunAsClient.Unix.cs +++ b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.RunAsClient.Unix.cs @@ -31,7 +31,7 @@ namespace System.IO.Pipes.Tests { string pipeName = Path.GetRandomFileName(); uint pairID = (uint)(Math.Abs(new Random(5125123).Next())); - RemoteInvoke(ServerConnectAsId, pipeName, pairID.ToString()).Dispose(); + RemoteInvoke(new Func(ServerConnectAsId), pipeName, pairID.ToString()).Dispose(); } private static int ServerConnectAsId(string pipeName, string pairIDString) @@ -39,7 +39,7 @@ namespace System.IO.Pipes.Tests uint pairID = uint.Parse(pairIDString); Assert.NotEqual(-1, seteuid(pairID)); using (var outbound = new NamedPipeServerStream(pipeName, PipeDirection.Out)) - using (var handle = RemoteInvoke(ClientConnectAsID, pipeName, pairIDString)) + using (var handle = RemoteInvoke(new Func(ClientConnectAsID), pipeName, pairIDString)) { // Connect as the unpriveleged user, but RunAsClient as the superuser outbound.WaitForConnection(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayHelpers.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayHelpers.cs index cbdb774..10224a5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayHelpers.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayHelpers.cs @@ -3,13 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; namespace System { internal static class ByteArrayHelpers { + // TODO #21395: + // Replace with the MemoryExtensions implementation of Equals once it's available internal static bool EqualsOrdinalAsciiIgnoreCase(string left, ReadOnlySpan right) { Debug.Assert(left != null, "Expected non-null string"); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaderParser.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaderParser.cs index bc50cad..ca9c27e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaderParser.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaderParser.cs @@ -8,9 +8,6 @@ using System.Diagnostics.Contracts; namespace System.Net.Http.Headers { -#if DEBUG - [ContractClass(typeof(HttpHeaderParserContract))] -#endif internal abstract class HttpHeaderParser { internal const string DefaultSeparator = ", "; @@ -91,25 +88,4 @@ namespace System.Net.Http.Headers return value.ToString(); } } - -#if DEBUG - [ContractClassFor(typeof(HttpHeaderParser))] - internal abstract class HttpHeaderParserContract : HttpHeaderParser - { - public HttpHeaderParserContract() - : base(false) - { - } - - public override bool TryParseValue(string value, object storeValue, ref int index, out object parsedValue) - { - // Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not - // allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not. - Debug.Assert((value == null) || ((index >= 0) && (index <= value.Length))); - - parsedValue = null; - return false; - } - } -#endif } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs index 32bd40b..e237df2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs @@ -102,12 +102,6 @@ namespace System.Net.Http } } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - } - public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -157,25 +151,17 @@ namespace System.Net.Http } } - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } + ValidateCopyToArgs(this, destination, bufferSize); - cancellationToken.ThrowIfCancellationRequested(); - - if (_connection == null) - { - // Response body fully consumed - return; - } + return cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + _connection != null ? CopyToAsyncCore(destination, bufferSize, cancellationToken) : + Task.CompletedTask; + } + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { CancellationTokenRegistration ctr = _connection.RegisterCancellation(cancellationToken); try { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs index 82682ac..6f19130 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs @@ -2,7 +2,6 @@ // 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.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -18,12 +17,6 @@ namespace System.Net.Http { } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ignored) - { - ValidateBufferArgs(buffer, offset, count); - return WriteAsync(new Memory(buffer, offset, count), ignored); - } - public override Task WriteAsync(ReadOnlyMemory source, CancellationToken ignored) { // The token is ignored because it's coming from SendAsync and the only operations @@ -78,10 +71,7 @@ namespace System.Net.Http // and if all of the headers and content fit in the write buffer that we've actually sent the request. await _connection.FlushAsync().ConfigureAwait(false); } - - public override Task FlushAsync(CancellationToken ignored) => // see comment on WriteAsync about "ignored" - _connection.FlushAsync(); - + public override async Task FinishAsync() { // Send 0 byte chunk to indicate end, then final CrLf diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs index 2c000f6..c278512 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionCloseReadStream.cs @@ -16,12 +16,6 @@ namespace System.Net.Http { } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - } - public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -74,25 +68,18 @@ namespace System.Net.Http return bytesRead; } - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - - cancellationToken.ThrowIfCancellationRequested(); + ValidateCopyToArgs(this, destination, bufferSize); - if (_connection == null) - { - // Response body fully consumed - return; - } + return + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + _connection != null ? CopyToAsyncCore(destination, bufferSize, cancellationToken) : + Task.CompletedTask; // null if response body fully consumed + } + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { Task copyTask = _connection.CopyToAsync(destination); if (!copyTask.IsCompletedSuccessfully) { @@ -109,13 +96,13 @@ namespace System.Net.Http { ctr.Dispose(); } - } - // If cancellation is requested and tears down the connection, it could cause the copy - // to end early but think it ended successfully. So we prioritize cancellation in this - // race condition, and if we find after the copy has completed that cancellation has - // been requested, we assume the copy completed due to cancellation and throw. - cancellationToken.ThrowIfCancellationRequested(); + // If cancellation is requested and tears down the connection, it could cause the copy + // to end early but think it ended successfully. So we prioritize cancellation in this + // race condition, and if we find after the copy has completed that cancellation has + // been requested, we assume the copy completed due to cancellation and throw. + cancellationToken.ThrowIfCancellationRequested(); + } // We cannot reuse this connection, so close it. _connection.Dispose(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs index e6daf28..a88da78 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs @@ -21,12 +21,6 @@ namespace System.Net.Http _contentBytesRemaining = contentLength; } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - } - public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -89,25 +83,18 @@ namespace System.Net.Http return bytesRead; } - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } + ValidateCopyToArgs(this, destination, bufferSize); - cancellationToken.ThrowIfCancellationRequested(); - - if (_connection == null) - { - // Response body fully consumed - return; - } + return + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + _connection != null ? CopyToAsyncCore(destination, bufferSize, cancellationToken) : + Task.CompletedTask; // null if response body fully consumed + } + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { Task copyTask = _connection.CopyToAsync(destination, _contentBytesRemaining); if (!copyTask.IsCompletedSuccessfully) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs index 507f883..2cc03da 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs @@ -15,12 +15,6 @@ namespace System.Net.Http { } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ignored) // token ignored as it comes from SendAsync - { - ValidateBufferArgs(buffer, offset, count); - return WriteAsync(new ReadOnlyMemory(buffer, offset, count), ignored); - } - public override Task WriteAsync(ReadOnlyMemory source, CancellationToken ignored) // token ignored as it comes from SendAsync { if (_connection._currentRequest == null) @@ -36,9 +30,6 @@ namespace System.Net.Http return _connection.WriteWithoutBufferingAsync(source); } - public override Task FlushAsync(CancellationToken ignored) => // token ignored as it comes from SendAsync - _connection.FlushAsync(); - public override Task FinishAsync() { _connection = null; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs index 41843de..f4bf6d4 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Http.Headers; @@ -23,12 +24,11 @@ namespace System.Net.Http public DecompressionHandler(DecompressionMethods decompressionMethods, HttpMessageHandler innerHandler) { - _innerHandler = innerHandler ?? throw new ArgumentNullException(nameof(innerHandler)); - if (decompressionMethods == DecompressionMethods.None) - { - throw new ArgumentOutOfRangeException(nameof(decompressionMethods)); - } + Debug.Assert(decompressionMethods != DecompressionMethods.None); + Debug.Assert(innerHandler != null); + _decompressionMethods = decompressionMethods; + _innerHandler = innerHandler; } internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs index 6320af3..471b3cd 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/EmptyReadStream.cs @@ -20,13 +20,9 @@ namespace System.Net.Http protected override void Dispose(bool disposing) { /* nop */ } public override void Close() { /* nop */ } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return cancellationToken.IsCancellationRequested ? - Task.FromCanceled(cancellationToken) : - s_zeroTask; - } + public override int ReadByte() => -1; + + public override int Read(Span destination) => 0; public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) => cancellationToken.IsCancellationRequested ? new ValueTask(Task.FromCanceled(cancellationToken)) : diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index f84afdf..c6cbcef 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -731,21 +731,21 @@ namespace System.Net.Http pos++; if (pos == line.Length) { - // Ignore invalid header line that doesn't contain ':'. - return; + // Invalid header line that doesn't contain ':'. + ThrowInvalidHttpResponse(); } } if (pos == 0) { - // Ignore invalid empty header name. - return; + // Invalid empty header name. + ThrowInvalidHttpResponse(); } if (!HeaderDescriptor.TryGet(line.Slice(0, pos), out HeaderDescriptor descriptor)) { - // Ignore invalid header name - return; + // Invalid header name + ThrowInvalidHttpResponse(); } // Eat any trailing whitespace @@ -754,15 +754,15 @@ namespace System.Net.Http pos++; if (pos == line.Length) { - // Ignore invalid header line that doesn't contain ':'. - return; + // Invalid header line that doesn't contain ':'. + ThrowInvalidHttpResponse(); } } if (line[pos++] != ':') { - // Ignore invalid header line that doesn't contain ':'. - return; + // Invalid header line that doesn't contain ':'. + ThrowInvalidHttpResponse(); } // Skip whitespace after colon diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentDuplexStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentDuplexStream.cs index 73e04ed..199383b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentDuplexStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentDuplexStream.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; +using System.Threading.Tasks; namespace System.Net.Http { @@ -24,8 +25,23 @@ namespace System.Net.Http return ReadAsync(new Memory(buffer, offset, count), CancellationToken.None).GetAwaiter().GetResult(); } - public sealed override void Write(byte[] buffer, int offset, int count) => - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + public sealed override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArgs(buffer, offset, count); + return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public sealed override void Write(byte[] buffer, int offset, int count) + { + ValidateBufferArgs(buffer, offset, count); + WriteAsync(new Memory(buffer, offset, count), CancellationToken.None).GetAwaiter().GetResult(); + } + + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArgs(buffer, offset, count); + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); + } public sealed override void CopyTo(Stream destination, int bufferSize) => CopyToAsync(destination, bufferSize, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentReadStream.cs index fdad589..f7dd5c8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentReadStream.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; +using System.Threading.Tasks; namespace System.Net.Http { @@ -17,8 +18,11 @@ namespace System.Net.Http public sealed override bool CanWrite => false; public sealed override void Flush() { } - + public sealed override void WriteByte(byte value) => throw new NotSupportedException(); public sealed override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public sealed override void Write(ReadOnlySpan source) => throw new NotSupportedException(); + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(); + public sealed override Task WriteAsync(ReadOnlyMemory destination, CancellationToken cancellationToken) => throw new NotSupportedException(); public sealed override int Read(byte[] buffer, int offset, int count) { @@ -26,6 +30,12 @@ namespace System.Net.Http return ReadAsync(new Memory(buffer, offset, count), CancellationToken.None).GetAwaiter().GetResult(); } + public sealed override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArgs(buffer, offset, count); + return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + public sealed override void CopyTo(Stream destination, int bufferSize) => CopyToAsync(destination, bufferSize, CancellationToken.None).GetAwaiter().GetResult(); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs index ad329ff..be3f2a1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.IO; -using System.Threading; using System.Threading.Tasks; namespace System.Net.Http @@ -34,13 +33,13 @@ namespace System.Net.Http public sealed override bool CanSeek => false; public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => - TaskToApm.Begin(ReadAsync(buffer, offset, count, default(CancellationToken)), callback, state); + TaskToApm.Begin(ReadAsync(buffer, offset, count, default), callback, state); public sealed override int EndRead(IAsyncResult asyncResult) => TaskToApm.End(asyncResult); public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) => - TaskToApm.Begin(WriteAsync(buffer, offset, count, default(CancellationToken)), callback, state); + TaskToApm.Begin(WriteAsync(buffer, offset, count, default), callback, state); public sealed override void EndWrite(IAsyncResult asyncResult) => TaskToApm.End(asyncResult); @@ -74,5 +73,29 @@ namespace System.Net.Http throw new ArgumentOutOfRangeException(nameof(count)); } } + + /// + /// Validate the arguments to CopyTo, as would Stream.CopyTo, but with knowledge that + /// the source stream is always readable and so only checking the destination. + /// + protected static void ValidateCopyToArgs(Stream source, Stream destination, int bufferSize) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, SR.ArgumentOutOfRange_NeedPosNum); + } + + if (!destination.CanWrite) + { + throw destination.CanRead ? + new NotSupportedException(SR.NotSupported_UnwritableStream) : + (Exception)new ObjectDisposedException(nameof(destination), SR.ObjectDisposed_StreamClosed); + } + } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs index b302e89..cf4c439 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs @@ -8,21 +8,37 @@ using System.Threading.Tasks; namespace System.Net.Http { - internal abstract class HttpContentWriteStream : HttpContentStream + internal partial class HttpConnection : IDisposable { - public HttpContentWriteStream(HttpConnection connection) : base(connection) => - Debug.Assert(connection != null); + private abstract class HttpContentWriteStream : HttpContentStream + { + public HttpContentWriteStream(HttpConnection connection) : base(connection) => + Debug.Assert(connection != null); - public sealed override bool CanRead => false; - public sealed override bool CanWrite => true; + public sealed override bool CanRead => false; + public sealed override bool CanWrite => true; - public sealed override void Flush() => FlushAsync().GetAwaiter().GetResult(); + public sealed override void Flush() => FlushAsync().GetAwaiter().GetResult(); - public sealed override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public sealed override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public sealed override void Write(byte[] buffer, int offset, int count) => - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + public sealed override void Write(byte[] buffer, int offset, int count) => + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - public abstract Task FinishAsync(); + // The token here is ignored because it's coming from SendAsync and the only operations + // here are those that are already covered by the token having been registered with + // to close the connection. + + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ignored) + { + ValidateBufferArgs(buffer, offset, count); + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), ignored); + } + + public sealed override Task FlushAsync(CancellationToken ignored) => + _connection.FlushAsync(); + + public abstract Task FinishAsync(); + } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpProxyConnectionHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpProxyConnectionHandler.cs index a1fd08e..fa81824 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpProxyConnectionHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpProxyConnectionHandler.cs @@ -58,7 +58,7 @@ namespace System.Net.Http { if (proxyUri.Scheme != UriScheme.Http) { - throw new InvalidOperationException(SR.net_http_invalid_proxy_scheme); + throw new NotSupportedException(SR.net_http_invalid_proxy_scheme); } HttpResponseMessage response = await GetConnectionAndSendAsync(request, proxyUri, cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpSystemProxy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpSystemProxy.cs index 80e0a90..1a7172f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpSystemProxy.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpSystemProxy.cs @@ -2,9 +2,6 @@ // 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; -using System.Net.Http; -using System.Net; using System.Collections.Generic; using System.Runtime.InteropServices; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs index e30f76d..1c4af8d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs @@ -16,12 +16,6 @@ namespace System.Net.Http { } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - } - public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -69,25 +63,17 @@ namespace System.Net.Http return bytesRead; } - public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - if (destination == null) - { - throw new ArgumentNullException(nameof(destination)); - } - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (_connection == null) - { - // Response body fully consumed - return; - } + ValidateCopyToArgs(this, destination, bufferSize); + return + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : + _connection != null ? CopyToAsyncCore(destination, bufferSize, cancellationToken) : + Task.CompletedTask; // null if response body fully consumed + } + private async Task CopyToAsyncCore(Stream destination, int bufferSize, CancellationToken cancellationToken) + { Task copyTask = _connection.CopyToAsync(destination); if (!copyTask.IsCompletedSuccessfully) { @@ -104,6 +90,12 @@ namespace System.Net.Http { ctr.Dispose(); } + + // If cancellation is requested and tears down the connection, it could cause the copy + // to end early but think it ended successfully. So we prioritize cancellation in this + // race condition, and if we find after the copy has completed that cancellation has + // been requested, we assume the copy completed due to cancellation and throw. + cancellationToken.ThrowIfCancellationRequested(); } // We cannot reuse this connection, so close it. @@ -111,12 +103,6 @@ namespace System.Net.Http _connection = null; } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArgs(buffer, offset, count); - return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); - } - public override Task WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs index 7c1be60..ab218bb 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/DiagnosticsTests.cs @@ -168,10 +168,12 @@ namespace System.Net.Http.Functional.Tests [ActiveIssue(23771, TestPlatforms.AnyUnix)] [OuterLoop] // TODO: Issue #11345 - [Fact] - public void SendAsync_HttpTracingEnabled_Succeeds() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SendAsync_HttpTracingEnabled_Succeeds(bool useSsl) { - RemoteInvoke(async useSocketsHttpHandlerString => + RemoteInvoke(async (useSocketsHttpHandlerString, useSslString) => { using (var listener = new TestEventListener("Microsoft-System-Net-Http", EventLevel.Verbose)) { @@ -191,9 +193,10 @@ namespace System.Net.Http.Functional.Tests // Do a post to a remote server byte[] expectedData = Enumerable.Range(0, 20000).Select(i => unchecked((byte)i)).ToArray(); - HttpContent content = new ByteArrayContent(expectedData); + Uri remoteServer = bool.Parse(useSslString) ? Configuration.Http.SecureRemoteEchoServer : Configuration.Http.RemoteEchoServer; + var content = new ByteArrayContent(expectedData); content.Headers.ContentMD5 = TestHelper.ComputeMD5Hash(expectedData); - using (HttpResponseMessage response = await client.PostAsync(Configuration.Http.RemoteEchoServer, content)) + using (HttpResponseMessage response = await client.PostAsync(remoteServer, content)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -208,7 +211,7 @@ namespace System.Net.Http.Functional.Tests } return SuccessExitCode; - }, UseSocketsHttpHandler.ToString()).Dispose(); + }, UseSocketsHttpHandler.ToString(), useSsl.ToString()).Dispose(); } [OuterLoop] // TODO: Issue #11345 diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.ClientCertificates.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.ClientCertificates.cs index d91e8fc..155b54e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.ClientCertificates.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.ClientCertificates.cs @@ -206,8 +206,8 @@ namespace System.Net.Http.Functional.Tests } [SkipOnTargetFramework(TargetFrameworkMonikers.Uap, "dotnet/corefx #20010")] - [OuterLoop] // TODO: Issue #11345 [ActiveIssue(9543)] // fails sporadically with 'WinHttpException : The server returned an invalid or unrecognized response' or 'TaskCanceledException : A task was canceled' + [OuterLoop] // TODO: Issue #11345 [Theory] [InlineData(6, false)] [InlineData(3, true)] @@ -221,6 +221,12 @@ namespace System.Net.Http.Functional.Tests return; } + if (!UseSocketsHttpHandler) + { + // Issue #9543: fails sporadically on WinHttpHandler/CurlHandler + return; + } + var options = new LoopbackServer.Options { UseSsl = true }; Func createClient = (cert) => @@ -228,6 +234,7 @@ namespace System.Net.Http.Functional.Tests HttpClientHandler handler = CreateHttpClientHandler(); handler.ServerCertificateCustomValidationCallback = delegate { return true; }; handler.ClientCertificates.Add(cert); + Assert.True(handler.ClientCertificates.Contains(cert)); return new HttpClient(handler); }; @@ -245,9 +252,9 @@ namespace System.Net.Http.Functional.Tests await LoopbackServer.CreateServerAsync(async (server, url) => { - if (reuseClient) + using (X509Certificate2 cert = Configuration.Certificates.GetClientCertificate()) { - using (X509Certificate2 cert = Configuration.Certificates.GetClientCertificate()) + if (reuseClient) { using (HttpClient client = createClient(cert)) { @@ -260,26 +267,56 @@ namespace System.Net.Http.Functional.Tests } } } - } - else - { - for (int i = 0; i < numberOfRequests; i++) + else { - using (X509Certificate2 cert = Configuration.Certificates.GetClientCertificate()) + for (int i = 0; i < numberOfRequests; i++) { using (HttpClient client = createClient(cert)) { await makeAndValidateRequest(client, server, url, cert); } - } - GC.Collect(); - GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } } }, options); } + [OuterLoop] // TODO: Issue #11345 + [Theory] + [InlineData(ClientCertificateOption.Manual)] + [InlineData(ClientCertificateOption.Automatic)] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "Fails with \"Authentication failed\" error.")] + public async Task AutomaticOrManual_DoesntFailRegardlessOfWhetherClientCertsAreAvailable(ClientCertificateOption mode) + { + if (!BackendSupportsCustomCertificateHandling) // can't use [Conditional*] right now as it's evaluated at the wrong time for SocketsHttpHandler + { + _output.WriteLine($"Skipping {nameof(Manual_CertificateSentMatchesCertificateReceived_Success)}()"); + return; + } + + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (var client = new HttpClient(handler)) + { + handler.ServerCertificateCustomValidationCallback = delegate { return true; }; + handler.ClientCertificateOptions = mode; + + await LoopbackServer.CreateServerAsync(async server => + { + Task clientTask = client.GetStringAsync(server.Uri); + Task serverTask = server.AcceptConnectionAsync(async connection => + { + SslStream sslStream = Assert.IsType(connection.Stream); + await connection.ReadRequestHeaderAndSendResponseAsync(); + }); + + await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed(); + }, new LoopbackServer.Options { UseSsl = true }); + } + } + private bool BackendSupportsCustomCertificateHandling => UseSocketsHttpHandler || new HttpClientHandler_ServerCertificates_Test().BackendSupportsCustomCertificateHandling; diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs index 16249df..d203918 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http.Headers; @@ -11,6 +10,7 @@ using System.Net.Sockets; using System.Net.Test.Common; using System.Runtime.InteropServices; using System.Security.Authentication; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -1015,6 +1015,309 @@ namespace System.Net.Http.Functional.Tests } } + [Theory] + [InlineData(":")] + [InlineData(" : ")] + [InlineData("\x1234: \x5678")] + [InlineData("nocolon")] + [InlineData("no colon")] + [InlineData("Content-Length ")] + public async Task GetAsync_InvalidHeaderNameValue_ThrowsHttpRequestException(string invalidHeader) + { + if (IsCurlHandler && invalidHeader.Contains(':')) + { + // Issue #27172 + // CurlHandler allows these headers as long as they have a colon. + return; + } + + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var client = CreateHttpClient()) + { + await Assert.ThrowsAsync(() => client.GetStringAsync(uri)); + } + }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync($"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n{invalidHeader}\r\n\r\nhello world")); + } + + [Fact] + public async Task PostAsync_ManyDifferentRequestHeaders_SentCorrectly() + { + if (IsWinHttpHandler) + { + // Issue #27171 + // Fails consistently with: + // System.InvalidCastException: "Unable to cast object of type 'System.Object[]' to type 'System.Net.Http.WinHttpRequestState'" + // This appears to be due to adding the Expect: 100-continue header, which causes winhttp + // to fail with a "The parameter is incorrect" error, which in turn causes the request to + // be torn down, and in doing so, we this this during disposal of the SafeWinHttpHandle. + return; + } + + // Using examples from https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields + // Exercises all exposed request.Headers and request.Content.Headers strongly-typed properties + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var client = CreateHttpClient()) + { + byte[] contentArray = Encoding.ASCII.GetBytes("hello world"); + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = new ByteArrayContent(contentArray) }; + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); + request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate")); + request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US")); + request.Headers.Add("Accept-Datetime", "Thu, 31 May 2007 20:35:00 GMT"); + request.Headers.Add("Access-Control-Request-Method", "GET"); + request.Headers.Add("Access-Control-Request-Headers", "GET"); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + request.Headers.CacheControl = new CacheControlHeaderValue() { NoCache = true }; + request.Headers.Connection.Add("close"); + request.Headers.Add("Cookie", "$Version=1; Skin=new"); + request.Content.Headers.ContentLength = contentArray.Length; + request.Content.Headers.ContentMD5 = MD5.Create().ComputeHash(contentArray); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + request.Headers.Date = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT"); + request.Headers.Expect.Add(new NameValueWithParametersHeaderValue("100-continue")); + request.Headers.Add("Forwarded", "for=192.0.2.60;proto=http;by=203.0.113.43"); + request.Headers.Add("From", "User Name "); + request.Headers.Host = "en.wikipedia.org:8080"; + request.Headers.IfMatch.Add(new EntityTagHeaderValue("\"37060cd8c284d8af7ad3082f209582d\"")); + request.Headers.IfModifiedSince = DateTimeOffset.Parse("Sat, 29 Oct 1994 19:43:31 GMT"); + request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue("\"737060cd8c284d8af7ad3082f209582d\"")); + request.Headers.IfRange = new RangeConditionHeaderValue(DateTimeOffset.Parse("Wed, 21 Oct 2015 07:28:00 GMT")); + request.Headers.IfUnmodifiedSince = DateTimeOffset.Parse("Sat, 29 Oct 1994 19:43:31 GMT"); + request.Headers.MaxForwards = 10; + request.Headers.Add("Origin", "http://www.example-social-network.com"); + request.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); + request.Headers.ProxyAuthorization = new AuthenticationHeaderValue("Basic", "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + request.Headers.Range = new RangeHeaderValue(500, 999); + request.Headers.Referrer = new Uri("http://en.wikipedia.org/wiki/Main_Page"); + request.Headers.TE.Add(new TransferCodingWithQualityHeaderValue("trailers")); + request.Headers.TE.Add(new TransferCodingWithQualityHeaderValue("deflate")); + request.Headers.Trailer.Add("MyTrailer"); + request.Headers.TransferEncoding.Add(new TransferCodingHeaderValue("chunked")); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue("Mozilla", "5.0"))); + request.Headers.Upgrade.Add(new ProductHeaderValue("HTTPS", "1.3")); + request.Headers.Upgrade.Add(new ProductHeaderValue("IRC", "6.9")); + request.Headers.Upgrade.Add(new ProductHeaderValue("RTA", "x11")); + request.Headers.Upgrade.Add(new ProductHeaderValue("websocket")); + request.Headers.Via.Add(new ViaHeaderValue("1.0", "fred")); + request.Headers.Via.Add(new ViaHeaderValue("1.1", "example.com", null, "(Apache/1.1)")); + request.Headers.Warning.Add(new WarningHeaderValue(199, "-", "\"Miscellaneous warning\"")); + request.Headers.Add("X-Requested-With", "XMLHttpRequest"); + request.Headers.Add("DNT", "1 (Do Not Track Enabled)"); + request.Headers.Add("X-Forwarded-For", "client1"); + request.Headers.Add("X-Forwarded-For", "proxy1"); + request.Headers.Add("X-Forwarded-For", "proxy2"); + request.Headers.Add("X-Forwarded-Host", "en.wikipedia.org:8080"); + request.Headers.Add("X-Forwarded-Proto", "https"); + request.Headers.Add("Front-End-Https", "https"); + request.Headers.Add("X-Http-Method-Override", "DELETE"); + request.Headers.Add("X-ATT-DeviceId", "GT-P7320/P7320XXLPG"); + request.Headers.Add("X-Wap-Profile", "http://wap.samsungmobile.com/uaprof/SGH-I777.xml"); + request.Headers.Add("Proxy-Connection", "keep-alive"); + request.Headers.Add("X-UIDH", "..."); + request.Headers.Add("X-Csrf-Token", "i8XNjC4b8KVok4uw5RftR38Wgp2BFwql"); + request.Headers.Add("X-Request-ID", "f058ebd6-02f7-4d3f-942e-904344e8cde5"); + request.Headers.Add("X-Request-ID", "f058ebd6-02f7-4d3f-942e-904344e8cde5"); + + (await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).Dispose(); + } + }, async server => + { + await server.AcceptConnectionAsync(async connection => + { + var headersSet = new HashSet(); + string line; + while (!string.IsNullOrEmpty(line = await connection.Reader.ReadLineAsync())) + { + Assert.True(headersSet.Add(line)); + } + + await connection.Writer.WriteAsync($"HTTP/1.1 200 OK\r\nDate: {DateTimeOffset.UtcNow:R}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); + while (await connection.Socket.ReceiveAsync(new ArraySegment(new byte[1000]), SocketFlags.None) > 0); + + Assert.Contains("Accept-Charset: utf-8", headersSet); + Assert.Contains("Accept-Encoding: gzip, deflate", headersSet); + Assert.Contains("Accept-Language: en-US", headersSet); + Assert.Contains("Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT", headersSet); + Assert.Contains("Access-Control-Request-Method: GET", headersSet); + Assert.Contains("Access-Control-Request-Headers: GET", headersSet); + Assert.Contains("Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", headersSet); + Assert.Contains("Cache-Control: no-cache", headersSet); + Assert.Contains("Connection: close", headersSet, StringComparer.OrdinalIgnoreCase); // NetFxHandler uses "Close" vs "close" + if (!IsNetfxHandler) + { + Assert.Contains("Cookie: $Version=1; Skin=new", headersSet); + } + Assert.Contains("Date: Tue, 15 Nov 1994 08:12:31 GMT", headersSet); + Assert.Contains("Expect: 100-continue", headersSet); + Assert.Contains("Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43", headersSet); + Assert.Contains("From: User Name ", headersSet); + Assert.Contains("Host: en.wikipedia.org:8080", headersSet); + Assert.Contains("If-Match: \"37060cd8c284d8af7ad3082f209582d\"", headersSet); + Assert.Contains("If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT", headersSet); + Assert.Contains("If-None-Match: \"737060cd8c284d8af7ad3082f209582d\"", headersSet); + Assert.Contains("If-Range: Wed, 21 Oct 2015 07:28:00 GMT", headersSet); + Assert.Contains("If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT", headersSet); + Assert.Contains("Max-Forwards: 10", headersSet); + Assert.Contains("Origin: http://www.example-social-network.com", headersSet); + Assert.Contains("Pragma: no-cache", headersSet); + Assert.Contains("Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", headersSet); + Assert.Contains("Range: bytes=500-999", headersSet); + Assert.Contains("Referer: http://en.wikipedia.org/wiki/Main_Page", headersSet); + Assert.Contains("TE: trailers, deflate", headersSet); + Assert.Contains("Trailer: MyTrailer", headersSet); + Assert.Contains("Transfer-Encoding: chunked", headersSet); + Assert.Contains("User-Agent: Mozilla/5.0", headersSet); + Assert.Contains("Upgrade: HTTPS/1.3, IRC/6.9, RTA/x11, websocket", headersSet); + Assert.Contains("Via: 1.0 fred, 1.1 example.com (Apache/1.1)", headersSet); + Assert.Contains("Warning: 199 - \"Miscellaneous warning\"", headersSet); + Assert.Contains("X-Requested-With: XMLHttpRequest", headersSet); + Assert.Contains("DNT: 1 (Do Not Track Enabled)", headersSet); + Assert.Contains("X-Forwarded-For: client1, proxy1, proxy2", headersSet); + Assert.Contains("X-Forwarded-Host: en.wikipedia.org:8080", headersSet); + Assert.Contains("X-Forwarded-Proto: https", headersSet); + Assert.Contains("Front-End-Https: https", headersSet); + Assert.Contains("X-Http-Method-Override: DELETE", headersSet); + Assert.Contains("X-ATT-DeviceId: GT-P7320/P7320XXLPG", headersSet); + Assert.Contains("X-Wap-Profile: http://wap.samsungmobile.com/uaprof/SGH-I777.xml", headersSet); + if (!IsNetfxHandler) + { + Assert.Contains("Proxy-Connection: keep-alive", headersSet); + } + Assert.Contains("X-UIDH: ...", headersSet); + Assert.Contains("X-Csrf-Token: i8XNjC4b8KVok4uw5RftR38Wgp2BFwql", headersSet); + Assert.Contains("X-Request-ID: f058ebd6-02f7-4d3f-942e-904344e8cde5, f058ebd6-02f7-4d3f-942e-904344e8cde5", headersSet); + }); + }); + } + + [Fact] + public async Task GetAsync_ManyDifferentResponseHeaders_ParsedCorrectly() + { + // Using examples from https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields + // Exercises all exposed response.Headers and response.Content.Headers strongly-typed properties + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var client = CreateHttpClient()) + using (HttpResponseMessage resp = await client.GetAsync(uri)) + { + Assert.Equal("1.1", resp.Version.ToString()); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Contains("*", resp.Headers.GetValues("Access-Control-Allow-Origin")); + Assert.Contains("text/example;charset=utf-8", resp.Headers.GetValues("Accept-Patch")); + Assert.Contains("bytes", resp.Headers.AcceptRanges); + Assert.Equal(TimeSpan.FromSeconds(12), resp.Headers.Age.GetValueOrDefault()); + Assert.Contains("GET", resp.Content.Headers.Allow); + Assert.Contains("HEAD", resp.Content.Headers.Allow); + Assert.Contains("http/1.1=\"http2.example.com:8001\"; ma=7200", resp.Headers.GetValues("Alt-Svc")); + Assert.Equal(TimeSpan.FromSeconds(3600), resp.Headers.CacheControl.MaxAge.GetValueOrDefault()); + Assert.Contains("close", resp.Headers.Connection); + Assert.True(resp.Headers.ConnectionClose.GetValueOrDefault()); + Assert.Equal("attachment", resp.Content.Headers.ContentDisposition.DispositionType); + Assert.Equal("\"fname.ext\"", resp.Content.Headers.ContentDisposition.FileName); + Assert.Contains("gzip", resp.Content.Headers.ContentEncoding); + Assert.Contains("da", resp.Content.Headers.ContentLanguage); + Assert.Equal(new Uri("/index.htm", UriKind.Relative), resp.Content.Headers.ContentLocation); + Assert.Equal(Convert.FromBase64String("Q2hlY2sgSW50ZWdyaXR5IQ=="), resp.Content.Headers.ContentMD5); + Assert.Equal("bytes", resp.Content.Headers.ContentRange.Unit); + Assert.Equal(21010, resp.Content.Headers.ContentRange.From.GetValueOrDefault()); + Assert.Equal(47021, resp.Content.Headers.ContentRange.To.GetValueOrDefault()); + Assert.Equal(47022, resp.Content.Headers.ContentRange.Length.GetValueOrDefault()); + Assert.Equal("text/html", resp.Content.Headers.ContentType.MediaType); + Assert.Equal("utf-8", resp.Content.Headers.ContentType.CharSet); + Assert.Equal(DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT"), resp.Headers.Date.GetValueOrDefault()); + Assert.Equal("\"737060cd8c284d8af7ad3082f209582d\"", resp.Headers.ETag.Tag); + Assert.Equal(DateTimeOffset.Parse("Thu, 01 Dec 1994 16:00:00 GMT"), resp.Content.Headers.Expires.GetValueOrDefault()); + Assert.Equal(DateTimeOffset.Parse("Tue, 15 Nov 1994 12:45:26 GMT"), resp.Content.Headers.LastModified.GetValueOrDefault()); + Assert.Contains("; rel=\"alternate\"", resp.Headers.GetValues("Link")); + Assert.Equal(new Uri("http://www.w3.org/pub/WWW/People.html"), resp.Headers.Location); + Assert.Contains("CP=\"This is not a P3P policy!\"", resp.Headers.GetValues("P3P")); + Assert.Contains(new NameValueHeaderValue("no-cache"), resp.Headers.Pragma); + Assert.Contains(new AuthenticationHeaderValue("basic"), resp.Headers.ProxyAuthenticate); + Assert.Contains("max-age=2592000; pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\"", resp.Headers.GetValues("Public-Key-Pins")); + Assert.Equal(TimeSpan.FromSeconds(120), resp.Headers.RetryAfter.Delta.GetValueOrDefault()); + Assert.Contains(new ProductInfoHeaderValue("Apache", "2.4.1"), resp.Headers.Server); + Assert.Contains("UserID=JohnDoe; Max-Age=3600; Version=1", resp.Headers.GetValues("Set-Cookie")); + Assert.Contains("max-age=16070400; includeSubDomains", resp.Headers.GetValues("Strict-Transport-Security")); + Assert.Contains("Max-Forwards", resp.Headers.Trailer); + Assert.Contains("?", resp.Headers.GetValues("Tk")); + Assert.Contains(new ProductHeaderValue("HTTPS", "1.3"), resp.Headers.Upgrade); + Assert.Contains(new ProductHeaderValue("IRC", "6.9"), resp.Headers.Upgrade); + Assert.Contains(new ProductHeaderValue("websocket"), resp.Headers.Upgrade); + Assert.Contains("Accept-Language", resp.Headers.Vary); + Assert.Contains(new ViaHeaderValue("1.0", "fred"), resp.Headers.Via); + Assert.Contains(new ViaHeaderValue("1.1", "example.com", null, "(Apache/1.1)"), resp.Headers.Via); + Assert.Contains(new WarningHeaderValue(199, "-", "\"Miscellaneous warning\"", DateTimeOffset.Parse("Wed, 21 Oct 2015 07:28:00 GMT")), resp.Headers.Warning); + Assert.Contains(new AuthenticationHeaderValue("Basic"), resp.Headers.WwwAuthenticate); + Assert.Contains("deny", resp.Headers.GetValues("X-Frame-Options")); + Assert.Contains("default-src 'self'", resp.Headers.GetValues("X-WebKit-CSP")); + Assert.Contains("5; url=http://www.w3.org/pub/WWW/People.html", resp.Headers.GetValues("Refresh")); + Assert.Contains("200 OK", resp.Headers.GetValues("Status")); + Assert.Contains("[, ]*", resp.Headers.GetValues("Timing-Allow-Origin")); + Assert.Contains("42.666", resp.Headers.GetValues("X-Content-Duration")); + Assert.Contains("nosniff", resp.Headers.GetValues("X-Content-Type-Options")); + Assert.Contains("PHP/5.4.0", resp.Headers.GetValues("X-Powered-By")); + Assert.Contains("f058ebd6-02f7-4d3f-942e-904344e8cde5", resp.Headers.GetValues("X-Request-ID")); + Assert.Contains("IE=EmulateIE7", resp.Headers.GetValues("X-UA-Compatible")); + Assert.Contains("1; mode=block", resp.Headers.GetValues("X-XSS-Protection")); + } + }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Access-Control-Allow-Origin: *\r\n" + + "Accept-Patch: text/example;charset=utf-8\r\n" + + "Accept-Ranges: bytes\r\n" + + "Age: 12\r\n" + + "Allow: GET, HEAD\r\n" + + "Alt-Svc: http/1.1=\"http2.example.com:8001\"; ma=7200\r\n" + + "Cache-Control: max-age=3600\r\n" + + "Connection: close\r\n" + + "Content-Disposition: attachment; filename=\"fname.ext\"\r\n" + + "Content-Encoding: gzip\r\n" + + "Content-Language: da\r\n" + + "Content-Location: /index.htm\r\n" + + "Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==\r\n" + + "Content-Range: bytes 21010-47021/47022\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n" + + "ETag: \"737060cd8c284d8af7ad3082f209582d\"\r\n" + + "Expires: Thu, 01 Dec 1994 16:00:00 GMT\r\n" + + "Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT\r\n" + + "Link: ; rel=\"alternate\"\r\n" + + "Location: http://www.w3.org/pub/WWW/People.html\r\n" + + "P3P: CP=\"This is not a P3P policy!\"\r\n" + + "Pragma: no-cache\r\n" + + "Proxy-Authenticate: Basic\r\n" + + "Public-Key-Pins: max-age=2592000; pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\"\r\n" + + "Retry-After: 120\r\n" + + "Server: Apache/2.4.1 (Unix)\r\n" + + "Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1\r\n" + + "Strict-Transport-Security: max-age=16070400; includeSubDomains\r\n" + + "Trailer: Max-Forwards\r\n" + + "Tk: ?\r\n" + + "Upgrade: HTTPS/1.3, IRC/6.9, RTA/x11, websocket\r\n" + + "Vary: Accept-Language\r\n" + + "Via: 1.0 fred, 1.1 example.com (Apache/1.1)\r\n" + + "Warning: 199 - \"Miscellaneous warning\" \"Wed, 21 Oct 2015 07:28:00 GMT\"\r\n" + + "WWW-Authenticate: Basic\r\n" + + "X-Frame-Options: deny\r\n" + + "X-WebKit-CSP: default-src 'self'\r\n" + + "Refresh: 5; url=http://www.w3.org/pub/WWW/People.html\r\n" + + "Status: 200 OK\r\n" + + "Timing-Allow-Origin: [, ]*\r\n" + + "Upgrade-Insecure-Requests: 1\r\n" + + "X-Content-Duration: 42.666\r\n" + + "X-Content-Type-Options: nosniff\r\n" + + "X-Powered-By: PHP/5.4.0\r\n" + + "X-Request-ID: f058ebd6-02f7-4d3f-942e-904344e8cde5\r\n" + + "X-UA-Compatible: IE=EmulateIE7\r\n" + + "X-XSS-Protection: 1; mode=block\r\n" + + "\r\n")); + } + [OuterLoop] // TODO: Issue #11345 [Theory] [InlineData(false)] @@ -1143,6 +1446,27 @@ namespace System.Net.Http.Functional.Tests } }); } + + [Fact] + public async Task GetAsync_InvalidChunkTerminator_ThrowsHttpRequestException() + { + await LoopbackServer.CreateClientAndServerAsync(async url => + { + using (HttpClient client = CreateHttpClient()) + { + await Assert.ThrowsAsync(() => client.GetAsync(url)); + } + }, server => server.AcceptConnectionSendCustomResponseAndCloseAsync( + "HTTP/1.1 200 OK\r\n" + + "Transfer-Encoding: chunked\r\n" + + "\r\n" + + "5\r\n" + + "hello" + // missing \r\n terminator + //"5\r\n" + + //"world" + // missing \r\n terminator + "0\r\n" + + "\r\n")); + } [OuterLoop] // TODO: Issue #11345 [Fact] @@ -1276,6 +1600,236 @@ namespace System.Net.Http.Functional.Tests }); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public async Task ReadAsStreamAsync_HandlerProducesWellBehavedResponseStream(bool? chunked) + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) + using (HttpResponseMessage response = await client.SendAsync(request, CancellationToken.None)) + { + using (Stream responseStream = await response.Content.ReadAsStreamAsync()) + { + Assert.Same(responseStream, await response.Content.ReadAsStreamAsync()); + + // Boolean properties returning correct values + Assert.True(responseStream.CanRead); + Assert.False(responseStream.CanWrite); + Assert.False(responseStream.CanSeek); + + // Not supported operations + Assert.Throws(() => responseStream.BeginWrite(new byte[1], 0, 1, null, null)); + Assert.Throws(() => responseStream.Length); + Assert.Throws(() => responseStream.Position); + Assert.Throws(() => responseStream.Position = 0); + Assert.Throws(() => responseStream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => responseStream.SetLength(0)); + Assert.Throws(() => responseStream.Write(new byte[1], 0, 1)); + Assert.Throws(() => responseStream.Write(new Span(new byte[1]))); + Assert.Throws(() => { responseStream.WriteAsync(new Memory(new byte[1])); }); + Assert.Throws(() => { responseStream.WriteAsync(new byte[1], 0, 1); }); + Assert.Throws(() => responseStream.WriteByte(1)); + + // Invalid arguments + var nonWritableStream = new MemoryStream(new byte[1], false); + var disposedStream = new MemoryStream(); + disposedStream.Dispose(); + Assert.Throws(() => responseStream.CopyTo(null)); + Assert.Throws(() => responseStream.CopyTo(Stream.Null, 0)); + Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(Stream.Null, 0, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(Stream.Null, -1, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(nonWritableStream, 100, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(disposedStream, 100, default); }); + Assert.Throws(() => responseStream.Read(null, 0, 100)); + Assert.Throws(() => responseStream.Read(new byte[1], -1, 1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 2, 1)); + Assert.Throws(() => responseStream.Read(new byte[1], 0, -1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 0, 2)); + Assert.Throws(() => responseStream.BeginRead(null, 0, 100, null, null)); + Assert.Throws(() => responseStream.BeginRead(new byte[1], -1, 1, null, null)); + Assert.ThrowsAny(() => responseStream.BeginRead(new byte[1], 2, 1, null, null)); + Assert.Throws(() => responseStream.BeginRead(new byte[1], 0, -1, null, null)); + Assert.ThrowsAny(() => responseStream.BeginRead(new byte[1], 0, 2, null, null)); + Assert.Throws(() => responseStream.EndRead(null)); + if (IsNetfxHandler) + { + // Argument exceptions on netfx are thrown out of these asynchronously rather than synchronously + await Assert.ThrowsAsync(() => responseStream.ReadAsync(null, 0, 100, default)); + await Assert.ThrowsAsync(() => responseStream.ReadAsync(new byte[1], -1, 1, default)); + await Assert.ThrowsAsync(() => responseStream.ReadAsync(new byte[1], 2, 1, default)); + await Assert.ThrowsAsync(() => responseStream.ReadAsync(new byte[1], 0, -1, default)); + await Assert.ThrowsAsync(() => responseStream.ReadAsync(new byte[1], 0, 2, default)); + } + else + { + Assert.Throws(() => { responseStream.ReadAsync(null, 0, 100, default); }); + Assert.Throws(() => { responseStream.ReadAsync(new byte[1], -1, 1, default); }); + Assert.ThrowsAny(() => { responseStream.ReadAsync(new byte[1], 2, 1, default); }); + Assert.Throws(() => { responseStream.ReadAsync(new byte[1], 0, -1, default); }); + Assert.ThrowsAny(() => { responseStream.ReadAsync(new byte[1], 0, 2, default); }); + } + + // Various forms of reading + var buffer = new byte[1]; + + Assert.Equal('h', responseStream.ReadByte()); + + Assert.Equal(1, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null)); + Assert.Equal((byte)'e', buffer[0]); + + Assert.Equal(1, await responseStream.ReadAsync(new Memory(buffer))); + Assert.Equal((byte)'l', buffer[0]); + + Assert.Equal(1, await responseStream.ReadAsync(buffer, 0, 1)); + Assert.Equal((byte)'l', buffer[0]); + + Assert.Equal(1, responseStream.Read(new Span(buffer))); + Assert.Equal((byte)'o', buffer[0]); + + Assert.Equal(1, responseStream.Read(buffer, 0, 1)); + Assert.Equal((byte)' ', buffer[0]); + + if (!IsNetfxHandler) + { + // Doing any of these 0-byte reads causes the connection to fail. + Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, Array.Empty(), 0, 0, null)); + Assert.Equal(0, await responseStream.ReadAsync(Memory.Empty)); + Assert.Equal(0, await responseStream.ReadAsync(Array.Empty(), 0, 0)); + Assert.Equal(0, responseStream.Read(Span.Empty)); + Assert.Equal(0, responseStream.Read(Array.Empty(), 0, 0)); + } + + // And copying + var ms = new MemoryStream(); + await responseStream.CopyToAsync(ms); + Assert.Equal("world", Encoding.ASCII.GetString(ms.ToArray())); + + // Read and copy again once we've exhausted all data + ms = new MemoryStream(); + await responseStream.CopyToAsync(ms); + responseStream.CopyTo(ms); + Assert.Equal(0, ms.Length); + Assert.Equal(-1, responseStream.ReadByte()); + Assert.Equal(0, responseStream.Read(buffer, 0, 1)); + Assert.Equal(0, responseStream.Read(new Span(buffer))); + Assert.Equal(0, await responseStream.ReadAsync(buffer, 0, 1)); + Assert.Equal(0, await responseStream.ReadAsync(new Memory(buffer))); + Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null)); + } + } + }, async server => + { + await server.AcceptConnectionAsync(async connection => + { + await connection.ReadRequestHeaderAsync(); + await connection.Writer.WriteAsync("HTTP/1.1 200 OK\r\n"); + switch (chunked) + { + case true: + await connection.Writer.WriteAsync("Transfer-Encoding: chunked\r\n\r\n3\r\nhel\r\n8\r\nlo world\r\n0\r\n\r\n"); + break; + + case false: + await connection.Writer.WriteAsync("Content-Length: 11\r\n\r\nhello world"); + break; + + case null: + await connection.Writer.WriteAsync("\r\nhello world"); + break; + } + }); + }); + } + + [Fact] + public async Task ReadAsStreamAsync_EmptyResponseBody_HandlerProducesWellBehavedResponseStream() + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + using (HttpResponseMessage response = await client.SendAsync(request, CancellationToken.None)) + using (Stream responseStream = await response.Content.ReadAsStreamAsync()) + { + // Boolean properties returning correct values + Assert.True(responseStream.CanRead); + Assert.False(responseStream.CanWrite); + Assert.False(responseStream.CanSeek); + + // Not supported operations + Assert.Throws(() => responseStream.BeginWrite(new byte[1], 0, 1, null, null)); + Assert.Throws(() => responseStream.Length); + Assert.Throws(() => responseStream.Position); + Assert.Throws(() => responseStream.Position = 0); + Assert.Throws(() => responseStream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => responseStream.SetLength(0)); + Assert.Throws(() => responseStream.Write(new byte[1], 0, 1)); + Assert.Throws(() => responseStream.Write(new Span(new byte[1]))); + await Assert.ThrowsAsync(() => responseStream.WriteAsync(new Memory(new byte[1]))); + await Assert.ThrowsAsync(() => responseStream.WriteAsync(new byte[1], 0, 1)); + Assert.Throws(() => responseStream.WriteByte(1)); + + // Invalid arguments + var nonWritableStream = new MemoryStream(new byte[1], false); + var disposedStream = new MemoryStream(); + disposedStream.Dispose(); + Assert.Throws(() => responseStream.CopyTo(null)); + Assert.Throws(() => responseStream.CopyTo(Stream.Null, 0)); + Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(Stream.Null, 0, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(Stream.Null, -1, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(nonWritableStream, 100, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(disposedStream, 100, default); }); + Assert.Throws(() => responseStream.Read(null, 0, 100)); + Assert.Throws(() => responseStream.Read(new byte[1], -1, 1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 2, 1)); + Assert.Throws(() => responseStream.Read(new byte[1], 0, -1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 0, 2)); + Assert.Throws(() => responseStream.BeginRead(null, 0, 100, null, null)); + Assert.Throws(() => responseStream.BeginRead(new byte[1], -1, 1, null, null)); + Assert.ThrowsAny(() => responseStream.BeginRead(new byte[1], 2, 1, null, null)); + Assert.Throws(() => responseStream.BeginRead(new byte[1], 0, -1, null, null)); + Assert.ThrowsAny(() => responseStream.BeginRead(new byte[1], 0, 2, null, null)); + Assert.Throws(() => responseStream.EndRead(null)); + if (!IsNetfxHandler) + { + // The netfx handler doesn't validate these arguments. + Assert.Throws(() => { responseStream.CopyTo(null); }); + Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { responseStream.Read(null, 0, 100); }); + Assert.Throws(() => { responseStream.ReadAsync(null, 0, 100, default); }); + Assert.Throws(() => { responseStream.BeginRead(null, 0, 100, null, null); }); + } + + // Empty reads + var buffer = new byte[1]; + Assert.Equal(-1, responseStream.ReadByte()); + Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null)); + Assert.Equal(0, await responseStream.ReadAsync(new Memory(buffer))); + Assert.Equal(0, await responseStream.ReadAsync(buffer, 0, 1)); + Assert.Equal(0, responseStream.Read(new Span(buffer))); + Assert.Equal(0, responseStream.Read(buffer, 0, 1)); + + // Empty copies + var ms = new MemoryStream(); + await responseStream.CopyToAsync(ms); + Assert.Equal(0, ms.Length); + responseStream.CopyTo(ms); + Assert.Equal(0, ms.Length); + } + } + }, + server => server.AcceptConnectionSendResponseAndCloseAsync()); + } + [OuterLoop] // TODO: Issue #11345 [Fact] public async Task Dispose_DisposingHandlerCancelsActiveOperationsWithoutResponses() @@ -2204,6 +2758,22 @@ namespace System.Net.Http.Functional.Tests } } + [Fact] + public async Task Proxy_SslProxyUnsupported_Throws() + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (var client = new HttpClient(handler)) + { + handler.Proxy = new WebProxy("https://" + Guid.NewGuid().ToString("N")); + + Type expectedType = IsNetfxHandler || UseSocketsHttpHandler ? + typeof(NotSupportedException) : + typeof(HttpRequestException); + + await Assert.ThrowsAsync(expectedType, () => client.GetAsync("http://" + Guid.NewGuid().ToString("N"))); + } + } + private static IEnumerable BypassedProxies() { yield return new object[] { null }; diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTestBase.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTestBase.cs index c896cfe..3acc5d0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTestBase.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTestBase.cs @@ -37,13 +37,26 @@ namespace System.Net.Http.Functional.Tests Debug.Assert(ctor != null, "Couldn't find test constructor on HttpClientHandler"); HttpClientHandler handler = (HttpClientHandler)ctor.Invoke(new object[] { useSocketsHttpHandler }); + Debug.Assert(useSocketsHttpHandler == IsSocketsHttpHandler(handler), "Unexpected handler."); + return handler; + } + + protected static bool IsSocketsHttpHandler(HttpClientHandler handler) + { FieldInfo field = typeof(HttpClientHandler).GetField("_socketsHttpHandler", BindingFlags.Instance | BindingFlags.NonPublic); - Debug.Assert(field != null, "Couldn't find _socketsHttpHandler field"); + if (field == null) + { + return false; + } + object socketsHttpHandler = field.GetValue(handler); - Debug.Assert((socketsHttpHandler != null) == useSocketsHttpHandler, $"{nameof(useSocketsHttpHandler)} was {useSocketsHttpHandler}, but _socketsHttpHandler field was {socketsHttpHandler}"); + if (socketsHttpHandler == null) + { + return false; + } - return handler; + return true; } } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpProtocolTests.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpProtocolTests.cs index 2ee1847..d348e36 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpProtocolTests.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpProtocolTests.cs @@ -459,18 +459,17 @@ namespace System.Net.Http.Functional.Tests [Fact] public async Task GetAsync_ReasonPhraseHasLF_BehaviorDifference() { - string responseString = "HTTP/1.1 200 O\nK"; + string responseString = "HTTP/1.1 200 O\n"; int expectedStatusCode = 200; string expectedReason = "O"; - if (IsWinHttpHandler || IsNetfxHandler || IsCurlHandler) + if (IsNetfxHandler) { - // WinHttpHandler, .NET Framework, and CurlHandler will throw HttpRequestException. + // .NET Framework will throw HttpRequestException. await GetAsyncThrowsExceptionHelper(responseString); } else { - // UAP and SocketsHttpHandler will allow LF ending. await GetAsyncSuccessHelper(responseString, expectedStatusCode, expectedReason); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 5041ad0..c1357e0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Security; using System.Net.Sockets; using System.Net.Test.Common; @@ -198,8 +199,35 @@ namespace System.Net.Http.Functional.Tests // Send a byte from the server to the client. The client will receive // the byte on its own, with HttpClient stripping away the chunk encoding. + // Read it in various ways. serverStream.WriteByte(i); - Assert.Equal(i, serverToClientStream.ReadByte()); + var buffer = new byte[1]; + switch (i % 6) + { + case 0: + Assert.Equal(i, serverToClientStream.ReadByte()); + break; + case 1: + Assert.Equal(1, serverToClientStream.Read(buffer, 0, 1)); + Assert.Equal(i, buffer[0]); + break; + case 2: + Assert.Equal(1, serverToClientStream.Read(new Span(buffer))); + Assert.Equal(i, buffer[0]); + break; + case 3: + Assert.Equal(1, await serverToClientStream.ReadAsync(buffer, 0, 1)); + Assert.Equal(i, buffer[0]); + break; + case 4: + Assert.Equal(1, await serverToClientStream.ReadAsync(new Memory(buffer))); + Assert.Equal(i, buffer[0]); + break; + case 5: + Assert.Equal(1, await Task.Factory.FromAsync(serverToClientStream.BeginRead, serverToClientStream.EndRead, buffer, 0, 1, null)); + Assert.Equal(i, buffer[0]); + break; + } } clientToServerStream.DoneWriting(); @@ -271,7 +299,7 @@ namespace System.Net.Http.Functional.Tests protected override bool UseSocketsHttpHandler => true; [Fact] - public async Task UpgradeConnection_Success() + public async Task UpgradeConnection_ReturnsReadableAndWritableStream() { await LoopbackServer.CreateServerAsync(async (server, url) => { @@ -287,28 +315,97 @@ namespace System.Net.Http.Functional.Tests using (Stream clientStream = await (await getResponseTask).Content.ReadAsStreamAsync()) { + // Boolean properties returning correct values Assert.True(clientStream.CanWrite); Assert.True(clientStream.CanRead); Assert.False(clientStream.CanSeek); - TextReader clientReader = new StreamReader(clientStream); - TextWriter clientWriter = new StreamWriter(clientStream) { AutoFlush = true }; - TextReader serverReader = connection.Reader; - TextWriter serverWriter = connection.Writer; - - const string helloServer = "hello server"; - const string helloClient = "hello client"; - const string goodbyeServer = "goodbye server"; - const string goodbyeClient = "goodbye client"; - - clientWriter.WriteLine(helloServer); - Assert.Equal(helloServer, serverReader.ReadLine()); - serverWriter.WriteLine(helloClient); - Assert.Equal(helloClient, clientReader.ReadLine()); - clientWriter.WriteLine(goodbyeServer); - Assert.Equal(goodbyeServer, serverReader.ReadLine()); - serverWriter.WriteLine(goodbyeClient); - Assert.Equal(goodbyeClient, clientReader.ReadLine()); + // Not supported operations + Assert.Throws(() => clientStream.Length); + Assert.Throws(() => clientStream.Position); + Assert.Throws(() => clientStream.Position = 0); + Assert.Throws(() => clientStream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => clientStream.SetLength(0)); + + // Invalid arguments + var nonWritableStream = new MemoryStream(new byte[1], false); + var disposedStream = new MemoryStream(); + disposedStream.Dispose(); + Assert.Throws(() => clientStream.CopyTo(null)); + Assert.Throws(() => clientStream.CopyTo(Stream.Null, 0)); + Assert.Throws(() => { clientStream.CopyToAsync(null, 100, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, 0, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(Stream.Null, -1, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(nonWritableStream, 100, default); }); + Assert.Throws(() => { clientStream.CopyToAsync(disposedStream, 100, default); }); + Assert.Throws(() => clientStream.Read(null, 0, 100)); + Assert.Throws(() => clientStream.Read(new byte[1], -1, 1)); + Assert.ThrowsAny(() => clientStream.Read(new byte[1], 2, 1)); + Assert.Throws(() => clientStream.Read(new byte[1], 0, -1)); + Assert.ThrowsAny(() => clientStream.Read(new byte[1], 0, 2)); + Assert.Throws(() => clientStream.BeginRead(null, 0, 100, null, null)); + Assert.Throws(() => clientStream.BeginRead(new byte[1], -1, 1, null, null)); + Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 2, 1, null, null)); + Assert.Throws(() => clientStream.BeginRead(new byte[1], 0, -1, null, null)); + Assert.ThrowsAny(() => clientStream.BeginRead(new byte[1], 0, 2, null, null)); + Assert.Throws(() => clientStream.EndRead(null)); + Assert.Throws(() => { clientStream.ReadAsync(null, 0, 100, default); }); + Assert.Throws(() => { clientStream.ReadAsync(new byte[1], -1, 1, default); }); + Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 2, 1, default); }); + Assert.Throws(() => { clientStream.ReadAsync(new byte[1], 0, -1, default); }); + Assert.ThrowsAny(() => { clientStream.ReadAsync(new byte[1], 0, 2, default); }); + + // Validate writing APIs on clientStream + + clientStream.WriteByte((byte)'!'); + clientStream.Write(new byte[] { (byte)'\r', (byte)'\n' }, 0, 2); + Assert.Equal("!", await connection.Reader.ReadLineAsync()); + + clientStream.Write(new Span(new byte[] { (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'\r', (byte)'\n' })); + Assert.Equal("hello", await connection.Reader.ReadLineAsync()); + + await clientStream.WriteAsync(new byte[] { (byte)'w', (byte)'o', (byte)'r', (byte)'l', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 7); + Assert.Equal("world", await connection.Reader.ReadLineAsync()); + + await clientStream.WriteAsync(new Memory(new byte[] { (byte)'a', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 5)); + Assert.Equal("and", await connection.Reader.ReadLineAsync()); + + await Task.Factory.FromAsync(clientStream.BeginWrite, clientStream.EndWrite, new byte[] { (byte)'b', (byte)'e', (byte)'y', (byte)'o', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 8, null); + Assert.Equal("beyond", await connection.Reader.ReadLineAsync()); + + clientStream.Flush(); + await clientStream.FlushAsync(); + + // Validate reading APIs on clientStream + await connection.Stream.WriteAsync(Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz")); + var buffer = new byte[1]; + + Assert.Equal('a', clientStream.ReadByte()); + + Assert.Equal(1, clientStream.Read(buffer, 0, 1)); + Assert.Equal((byte)'b', buffer[0]); + + Assert.Equal(1, clientStream.Read(new Span(buffer, 0, 1))); + Assert.Equal((byte)'c', buffer[0]); + + Assert.Equal(1, await clientStream.ReadAsync(buffer, 0, 1)); + Assert.Equal((byte)'d', buffer[0]); + + Assert.Equal(1, await clientStream.ReadAsync(new Memory(buffer, 0, 1))); + Assert.Equal((byte)'e', buffer[0]); + + Assert.Equal(1, await Task.Factory.FromAsync(clientStream.BeginRead, clientStream.EndRead, buffer, 0, 1, null)); + Assert.Equal((byte)'f', buffer[0]); + + var ms = new MemoryStream(); + Task copyTask = clientStream.CopyToAsync(ms); + + string bigString = string.Concat(Enumerable.Repeat("abcdefghijklmnopqrstuvwxyz", 1000)); + Task lotsOfDataSent = connection.Socket.SendAsync(Encoding.ASCII.GetBytes(bigString), SocketFlags.None); + connection.Socket.Shutdown(SocketShutdown.Send); + await copyTask; + await lotsOfDataSent; + Assert.Equal("ghijklmnopqrstuvwxyz" + bigString, Encoding.ASCII.GetString(ms.ToArray())); } }); } @@ -791,4 +888,84 @@ namespace System.Net.Http.Functional.Tests } } } + + public sealed class SocketsHttpHandler_ExternalConfiguration_Test : HttpClientTestBase + { + private const string EnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER"; + private const string AppContextSettingName = "System.Net.Http.UseSocketsHttpHandler"; + + private static bool UseSocketsHttpHandlerEnvironmentVariableIsNotSet => + string.IsNullOrEmpty(Environment.GetEnvironmentVariable(EnvironmentVariableSettingName)); + + [ConditionalTheory(nameof(UseSocketsHttpHandlerEnvironmentVariableIsNotSet))] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("tRuE", true)] + [InlineData("1", true)] + [InlineData("0", false)] + [InlineData("false", false)] + [InlineData("helloworld", false)] + [InlineData("", false)] + public void HttpClientHandler_SettingEnvironmentVariableChangesDefault(string envVarValue, bool expectedUseSocketsHandler) + { + RemoteInvoke((innerEnvVarValue, innerExpectedUseSocketsHandler) => + { + Environment.SetEnvironmentVariable(EnvironmentVariableSettingName, innerEnvVarValue); + using (var handler = new HttpClientHandler()) + { + Assert.Equal(bool.Parse(innerExpectedUseSocketsHandler), IsSocketsHttpHandler(handler)); + } + return SuccessExitCode; + }, envVarValue, expectedUseSocketsHandler.ToString()).Dispose(); + } + + [Fact] + public void HttpClientHandler_SettingAppContextChangesDefault() + { + RemoteInvoke(() => + { + AppContext.SetSwitch(AppContextSettingName, isEnabled: true); + using (var handler = new HttpClientHandler()) + { + Assert.True(IsSocketsHttpHandler(handler)); + } + + AppContext.SetSwitch(AppContextSettingName, isEnabled: false); + using (var handler = new HttpClientHandler()) + { + Assert.False(IsSocketsHttpHandler(handler)); + } + + return SuccessExitCode; + }).Dispose(); + } + + [Fact] + public void HttpClientHandler_AppContextOverridesEnvironmentVariable() + { + RemoteInvoke(() => + { + Environment.SetEnvironmentVariable(EnvironmentVariableSettingName, "true"); + using (var handler = new HttpClientHandler()) + { + Assert.True(IsSocketsHttpHandler(handler)); + } + + AppContext.SetSwitch(AppContextSettingName, isEnabled: false); + using (var handler = new HttpClientHandler()) + { + Assert.False(IsSocketsHttpHandler(handler)); + } + + AppContext.SetSwitch(AppContextSettingName, isEnabled: true); + Environment.SetEnvironmentVariable(EnvironmentVariableSettingName, null); + using (var handler = new HttpClientHandler()) + { + Assert.True(IsSocketsHttpHandler(handler)); + } + + return SuccessExitCode; + }).Dispose(); + } + } } diff --git a/src/libraries/System.Threading/tests/SemaphoreTests.cs b/src/libraries/System.Threading/tests/SemaphoreTests.cs index 7d88d42..6ef952e 100644 --- a/src/libraries/System.Threading/tests/SemaphoreTests.cs +++ b/src/libraries/System.Threading/tests/SemaphoreTests.cs @@ -290,7 +290,7 @@ namespace System.Threading.Tests // Create the two semaphores and the other process with which to synchronize using (var inbound = new Semaphore(1, 1, inboundName)) using (var outbound = new Semaphore(0, 1, outboundName)) - using (var remote = RemoteInvoke(PingPong_OtherProcess, outboundName, inboundName)) + using (var remote = RemoteInvoke(new Func(PingPong_OtherProcess), outboundName, inboundName)) { // Repeatedly wait for count in one semaphore and then release count into the other for (int i = 0; i < 10; i++) -- 2.7.4