From 707ce929bd72f0bc8cf64f219db10f3cf3dd38b1 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Thu, 12 Jan 2023 15:59:16 +0100 Subject: [PATCH 1/2] Enforce HttpClient limits on GetFromJsonAsync --- .../tests/System/Net/Http/LoopbackServer.cs | 4 +- .../Http/Json/HttpClientJsonExtensions.Get.cs | 16 ++-- .../HttpClientJsonExtensionsTests.cs | 87 +++++++++++++++++++ ...stem.Net.Http.Json.Functional.Tests.csproj | 4 +- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index c559ffe762c00..980c88b6587df 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -726,7 +726,7 @@ private async Task> ReadRequestHeaderBytesAsync() public async Task WriteStringAsync(string s) { byte[] bytes = Encoding.ASCII.GetBytes(s); - await _stream.WriteAsync(bytes); + await _stream.WriteAsync(bytes, 0, bytes.Length); } public async Task SendResponseAsync(string response) @@ -736,7 +736,7 @@ public async Task SendResponseAsync(string response) public async Task SendResponseAsync(byte[] response) { - await _stream.WriteAsync(response); + await _stream.WriteAsync(response, 0, response.Length); } public async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) diff --git a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs index 3df50de00fa83..1f4c01573c2e6 100644 --- a/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs +++ b/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Get.cs @@ -24,7 +24,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -37,7 +37,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, options, cancellationToken); } @@ -50,7 +50,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } @@ -63,7 +63,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, options, cancellationToken); } @@ -74,7 +74,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, context, cancellationToken); } @@ -85,7 +85,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, type, context, cancellationToken); } @@ -96,7 +96,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, jsonTypeInfo, cancellationToken); } @@ -107,7 +107,7 @@ public static partial class HttpClientJsonExtensions throw new ArgumentNullException(nameof(client)); } - Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + Task taskResponse = client.GetAsync(requestUri, HttpCompletionOption.ResponseContentRead, cancellationToken); return GetFromJsonAsyncCore(taskResponse, jsonTypeInfo, cancellationToken); } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs index 076c223877384..6c142dfe28ac3 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/HttpClientJsonExtensionsTests.cs @@ -354,5 +354,92 @@ await HttpMessageHandlerLoopbackServer.CreateClientAndServerAsync( await server.HandleRequestAsync(content: json, headers: headers); }); } + + public static IEnumerable GetFromJsonAsync_EnforcesMaxResponseContentBufferSize_MemberData() => + from useDeleteAsync in new[] { true, false } + from limit in new[] { 2, 100, 100000 } + from contentLength in new[] { limit, limit + 1 } + from chunked in new[] { true, false } + select new object[] { useDeleteAsync, limit, contentLength, chunked }; + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] // No Socket support + [MemberData(nameof(GetFromJsonAsync_EnforcesMaxResponseContentBufferSize_MemberData))] + public async Task GetFromJsonAsync_EnforcesMaxResponseContentBufferSize(bool useDeleteAsync, int limit, int contentLength, bool chunked) + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using var client = new HttpClient { MaxResponseContentBufferSize = limit }; + + Func testMethod = () => useDeleteAsync ? client.DeleteFromJsonAsync(uri) : client.GetFromJsonAsync(uri); + + if (contentLength > limit) + { + Exception ex = await Assert.ThrowsAsync(testMethod); + Assert.Contains(limit.ToString(), ex.Message); + } + else + { + await testMethod(); + } + }, + async server => + { + List headers = new(); + string content = $"\"{new string('a', contentLength - 2)}\""; + + if (chunked) + { + headers.Add(new HttpHeaderData("Transfer-Encoding", "chunked")); + content = $"{Convert.ToString(contentLength, 16)}\r\n{content}\r\n0\r\n\r\n"; + } + + await server.HandleRequestAsync(headers: headers, content: content); + }); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] // No Socket support + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task GetFromJsonAsync_EnforcesTimeout(bool useDeleteAsync, bool slowHeaders) + { + TaskCompletionSource exceptionThrown = new(TaskCreationOptions.RunContinuationsAsynchronously); + + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + using var client = new HttpClient { Timeout = TimeSpan.FromMilliseconds(100) }; + + Exception ex = await Assert.ThrowsAsync(() => + useDeleteAsync ? client.DeleteFromJsonAsync(uri) : client.GetFromJsonAsync(uri)); + +#if NETCORE + Assert.Contains("HttpClient.Timeout", ex.Message); + Assert.IsType(ex.InnerException); +#endif + + exceptionThrown.SetResult(0); + }, + async server => + { + // The client may timeout before even connecting the server + await Task.WhenAny(exceptionThrown.Task, Task.Run(async () => + { + try + { + await server.AcceptConnectionAsync(async connection => + { + if (!slowHeaders) + { + await connection.SendPartialResponseHeadersAsync(headers: new[] { new HttpHeaderData("Content-Length", "42") }); + } + + await exceptionThrown.Task; + }); + } + catch { } + })); + }); + } } } diff --git a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj index 144cbb55fd2b6..25c6170e27100 100644 --- a/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http.Json/tests/FunctionalTests/System.Net.Http.Json.Functional.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);net48 @@ -13,11 +13,13 @@ + + From 60ccc89687eb53ee00ed28e11d764f5200ff6f93 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Thu, 12 Jan 2023 16:02:08 +0100 Subject: [PATCH 2/2] Update csproj properties --- .../System.Net.Http.Json/src/System.Net.Http.Json.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj index 067db94007d3b..43bed3bd9f32c 100644 --- a/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj +++ b/src/libraries/System.Net.Http.Json/src/System.Net.Http.Json.csproj @@ -2,6 +2,8 @@ $(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) true + true + 1 Provides extension methods for System.Net.Http.HttpClient and System.Net.Http.HttpContent that perform automatic serialization and deserialization using System.Text.Json. Commonly Used Types: