From c2274eb07dcd179d9ebf13fd1215732bf216696d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 25 Feb 2023 21:43:59 +0800 Subject: [PATCH] Add .editorconfig and fix warnings (#2055) --- .editorconfig | 406 ++++++++++++ .../QpsWorker/Infrastructure/TimeStats.cs | 3 +- .../Internal/HttpContextStreamWriter.cs | 4 +- .../Internal/PipeExtensions.cs | 9 +- .../Internal/ReadOnlySequenceStream.cs | 21 +- src/Grpc.Reflection/SymbolRegistry.cs | 7 +- .../Infrastructure/LoopbackProxyServer.cs | 578 +++++++++--------- .../GrpcHttpClientBuilderExtensionsTests.cs | 3 +- test/dotnet-grpc.Tests/CommandBaseTests.cs | 1 - 9 files changed, 726 insertions(+), 306 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2a84037b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,406 @@ +; EditorConfig to support per-solution formatting. +; Use the EditorConfig VS add-in to make this work. +; http://editorconfig.org/ +; +; Here are some resources for what's supported for .NET/C# +; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +; https://learn.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +; +; Be **careful** editing this because some of the rules don't support adding a severity level +; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`) +; then the rule will be silently ignored. + +; This is the default for the codeline. +root = true + +[*] +indent_style = space +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.cs] +indent_size = 4 +dotnet_sort_system_directives_first = true + +# Don't use this. qualifier +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion + +# use int x = .. over Int32 +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +# use int.MaxValue over Int32.MaxValue +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Require var all the time. +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Disallow throw expressions. +csharp_style_throw_expression = false:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Namespace settings +csharp_style_namespace_declarations = file_scoped + +# Brace settings +csharp_prefer_braces = true # Prefer curly braces even for one line of code + +[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.{ps1,psm1}] +indent_size = 4 + +[*.sh] +indent_size = 4 +end_of_line = lf + +[*.{razor,cshtml}] +charset = utf-8-bom + +[*.{cs,vb}] + +# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1054.severity = warning + +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = warning + +# CA1047: Do not declare protected member in sealed type +dotnet_diagnostic.CA1047.severity = warning + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = warning + +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = warning + +# CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = warning + +# CA1511: Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = warning + +# CA1512: Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = warning + +# CA1513: Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = warning + +# CA1725: Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = suggestion + +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning + +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = warning + +# CA1810: Do not initialize unnecessarily +dotnet_diagnostic.CA1810.severity = warning + +# CA1821: Remove empty Finalizers +dotnet_diagnostic.CA1821.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning +dotnet_code_quality.CA1822.api_surface = private, internal + +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning + +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly +dotnet_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +dotnet_diagnostic.CA1828.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_diagnostic.CA1830.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1831.severity = warning +dotnet_diagnostic.CA1832.severity = warning +dotnet_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = warning + +# CA1836: Prefer IsEmpty over Count +dotnet_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning + +# CA1839: Use 'Environment.ProcessPath' +dotnet_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' +dotnet_diagnostic.CA1840.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task +dotnet_diagnostic.CA1842.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +dotnet_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1844.severity = warning + +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = warning + +# CA1846: Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = warning + +# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = warning + +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = warning + +# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = warning + +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = warning + +# CA1856: Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1856.severity = error + +# CA1857: A constant is expected for the parameter +dotnet_diagnostic.CA1857.severity = warning + +# CA1858: Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1858.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = warning + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = warning + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_diagnostic.CA2009.severity = warning + +# CA2011: Avoid infinite recursion +dotnet_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTask correctly +dotnet_diagnostic.CA2012.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2013.severity = warning + +# CA2014: Do not use stackalloc in loops. +dotnet_diagnostic.CA2014.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods that take one +dotnet_diagnostic.CA2016.severity = warning + +# CA2200: Rethrow to preserve stack details +dotnet_diagnostic.CA2200.severity = warning + +# CA2208: Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = warning + +# CA2245: Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = warning + +# CA2246: Assigning symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# CA2249: Use string.Contains instead of string.IndexOf to improve readability. +dotnet_diagnostic.CA2249.severity = warning + +# IDE0005: Remove unnecessary usings +dotnet_diagnostic.IDE0005.severity = warning + +# IDE0011: Curly braces to surround blocks of code +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDE0020.severity = warning + +# IDE0029: Use coalesce expression (non-nullable types) +dotnet_diagnostic.IDE0029.severity = warning + +# IDE0030: Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = warning + +# IDE0031: Use null propagation +dotnet_diagnostic.IDE0031.severity = warning + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0055: All formatting rules +dotnet_diagnostic.IDE0055.severity = suggestion + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_code_quality_unused_parameters = non_public +dotnet_diagnostic.IDE0060.severity = warning + +# IDE0062: Make local function static +dotnet_diagnostic.IDE0062.severity = warning + +# IDE0161: Convert to file-scoped namespace +dotnet_diagnostic.IDE0161.severity = warning + +# IDE0200: Lambda expression can be removed +dotnet_diagnostic.IDE0200.severity = warning + +# IDE2000: Disallow multiple blank lines +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_diagnostic.IDE2000.severity = warning + +[{test,tests,testassets,examples,perf,scripts,stress}/**.cs] +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = suggestion +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = suggestion +# CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = suggestion +# CA1511: Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = suggestion +# CA1512: Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = suggestion +# CA1513: Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = suggestion +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = suggestion +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = suggestion +# CA1810: Do not initialize unnecessarily +dotnet_diagnostic.CA1810.severity = suggestion +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = suggestion +# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly +dotnet_diagnostic.CA1826.severity = suggestion +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = suggestion +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = suggestion +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = suggestion +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = suggestion +# CA1837: Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = suggestion +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = suggestion +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = suggestion +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1844.severity = suggestion +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = suggestion +# CA1846: Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = suggestion +# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = suggestion +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = suggestion +# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = suggestion +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = suggestion +# CA1856: Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1856.severity = suggestion +# CA1857: A constant is expected for the parameter +dotnet_diagnostic.CA1857.severity = suggestion +# CA1858: Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1858.severity = suggestion +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = suggestion +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = suggestion +# CA2012: Use ValueTask correctly +dotnet_diagnostic.CA2012.severity = suggestion +# CA2249: Use string.Contains instead of string.IndexOf to improve readability. +dotnet_diagnostic.CA2249.severity = suggestion +# IDE0005: Remove unnecessary usings +dotnet_diagnostic.IDE0005.severity = suggestion +# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDE0020.severity = suggestion +# IDE0029: Use coalesce expression (non-nullable types) +dotnet_diagnostic.IDE0029.severity = suggestion +# IDE0030: Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = suggestion +# IDE0031: Use null propagation +dotnet_diagnostic.IDE0031.severity = suggestion +# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = suggestion +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = suggestion +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = suggestion +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = suggestion +# IDE0060: Remove unused parameters +dotnet_diagnostic.IDE0060.severity = suggestion +# IDE0062: Make local function static +dotnet_diagnostic.IDE0062.severity = suggestion +# IDE0200: Lambda expression can be removed +dotnet_diagnostic.IDE0200.severity = suggestion + +# CA2016: Forward the 'CancellationToken' parameter to methods that take one +dotnet_diagnostic.CA2016.severity = suggestion diff --git a/perf/benchmarkapps/QpsWorker/Infrastructure/TimeStats.cs b/perf/benchmarkapps/QpsWorker/Infrastructure/TimeStats.cs index f3fe58bbc..e3bc0454f 100644 --- a/perf/benchmarkapps/QpsWorker/Infrastructure/TimeStats.cs +++ b/perf/benchmarkapps/QpsWorker/Infrastructure/TimeStats.cs @@ -17,6 +17,7 @@ #endregion using System.Diagnostics; +using System.Globalization; // Copied from https://github.com/grpc/grpc/tree/master/src/csharp/Grpc.IntegrationTesting namespace QpsWorker.Infrastructure; @@ -72,7 +73,7 @@ public Snapshot(TimeSpan wallClockTime, TimeSpan userProcessorTime, TimeSpan pri public override string ToString() { - return string.Format("[TimeStats.Snapshot: wallClock {0}, userProcessor {1}, privilegedProcessor {2}]", WallClockTime, UserProcessorTime, PrivilegedProcessorTime); + return string.Format(CultureInfo.InvariantCulture, "[TimeStats.Snapshot: wallClock {0}, userProcessor {1}, privilegedProcessor {2}]", WallClockTime, UserProcessorTime, PrivilegedProcessorTime); } } } diff --git a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs index 44b9f6ab1..05719a91d 100644 --- a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs +++ b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -17,8 +17,6 @@ #endregion using Grpc.Core; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; namespace Grpc.AspNetCore.Server.Internal; diff --git a/src/Grpc.AspNetCore.Server/Internal/PipeExtensions.cs b/src/Grpc.AspNetCore.Server/Internal/PipeExtensions.cs index 977f31202..a08285337 100644 --- a/src/Grpc.AspNetCore.Server/Internal/PipeExtensions.cs +++ b/src/Grpc.AspNetCore.Server/Internal/PipeExtensions.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -19,9 +19,10 @@ using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; +#if NET6_0_OR_GREATER using System.Runtime.CompilerServices; +#endif using Grpc.Core; using Grpc.Net.Compression; using Microsoft.Extensions.Logging; @@ -87,7 +88,7 @@ public static async Task WriteStreamedMessageAsync(this PipeWriter pi var httpResponse = serverCallContext.HttpContext.Response; if (!httpResponse.HasStarted) { - await httpResponse.StartAsync(); + await httpResponse.StartAsync(cancellationToken); } GrpcServerLog.SendingMessage(logger); @@ -102,7 +103,7 @@ public static async Task WriteStreamedMessageAsync(this PipeWriter pi if (flush) { - var flushResult = await pipeWriter.FlushAsync(); + var flushResult = await pipeWriter.FlushAsync(cancellationToken); // Workaround bug where FlushAsync doesn't return IsCanceled = true on request abort. // https://github.com/dotnet/aspnetcore/issues/40788 diff --git a/src/Grpc.AspNetCore.Server/Internal/ReadOnlySequenceStream.cs b/src/Grpc.AspNetCore.Server/Internal/ReadOnlySequenceStream.cs index 27cab0c2f..bd196e9f7 100644 --- a/src/Grpc.AspNetCore.Server/Internal/ReadOnlySequenceStream.cs +++ b/src/Grpc.AspNetCore.Server/Internal/ReadOnlySequenceStream.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -62,18 +62,23 @@ public override Task FlushAsync(CancellationToken cancellationToken) } public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } + + public override int Read(Span buffer) { var remaining = _readOnlySequence.Slice(_position); - var toCopy = remaining.Slice(0, Math.Min(count, remaining.Length)); + var toCopy = remaining.Slice(0, Math.Min(buffer.Length, remaining.Length)); _position = toCopy.End; - toCopy.CopyTo(buffer.AsSpan(offset, count)); + toCopy.CopyTo(buffer); return (int)toCopy.Length; } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var bytesRead = Read(buffer, offset, count); + var bytesRead = Read(buffer.AsSpan(offset, count)); if (bytesRead == 0) { return TaskOfZero; @@ -89,6 +94,12 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel } } + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(Read(buffer.Span)); + } + public override int ReadByte() { var remaining = _readOnlySequence.Slice(_position); @@ -152,6 +163,8 @@ public override long Seek(long offset, SeekOrigin origin) public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(); + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { foreach (var segment in _readOnlySequence) diff --git a/src/Grpc.Reflection/SymbolRegistry.cs b/src/Grpc.Reflection/SymbolRegistry.cs index a6310a00e..12f1abacd 100644 --- a/src/Grpc.Reflection/SymbolRegistry.cs +++ b/src/Grpc.Reflection/SymbolRegistry.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,8 @@ // limitations under the License. #endregion -using System.Collections.Generic; -using Grpc.Core.Utils; using Google.Protobuf.Reflection; +using Grpc.Core.Utils; namespace Grpc.Reflection; @@ -71,7 +70,7 @@ public FileDescriptor FileContainingSymbol(string symbol) /// /// Builder class which isn't exposed, but acts as a convenient alternative to passing round two dictionaries in recursive calls. /// - private class Builder + private sealed class Builder { private readonly Dictionary filesByName; private readonly Dictionary filesBySymbol; diff --git a/test/FunctionalTests/Infrastructure/LoopbackProxyServer.cs b/test/FunctionalTests/Infrastructure/LoopbackProxyServer.cs index d2dbfdb67..99f0f739a 100644 --- a/test/FunctionalTests/Infrastructure/LoopbackProxyServer.cs +++ b/test/FunctionalTests/Infrastructure/LoopbackProxyServer.cs @@ -17,6 +17,7 @@ #endregion using System.Collections.Concurrent; +using System.Globalization; using System.Net; using System.Net.Sockets; using System.Runtime.ExceptionServices; @@ -27,368 +28,369 @@ #nullable disable // Copied with permission from https://github.com/dotnet/runtime/blob/a1c772dc866bf61dab891d9b68a0377367d191f8/src/libraries/Common/tests/System/Net/Http/LoopbackProxyServer.cs -namespace Grpc.AspNetCore.FunctionalTests.Infrastructure +namespace Grpc.AspNetCore.FunctionalTests.Infrastructure; + +/// +/// Provides a test-only HTTP proxy. Handles multiple connections/requests and CONNECT tunneling for HTTPS +/// endpoints. Provides simulated proxy authentication for Basic, Digest, NTLM, and Negotiate schemes by +/// checking only for a 'Proxy-Authorization' request header. +/// +public sealed class LoopbackProxyServer : IDisposable { - /// - /// Provides a test-only HTTP proxy. Handles multiple connections/requests and CONNECT tunneling for HTTPS - /// endpoints. Provides simulated proxy authentication for Basic, Digest, NTLM, and Negotiate schemes by - /// checking only for a 'Proxy-Authorization' request header. - /// - public sealed class LoopbackProxyServer : IDisposable + private const string ProxyAuthorizationHeader = "Proxy-Authorization"; + private const string ProxyAuthenticateBasicHeader = "Proxy-Authenticate: Basic realm=\"NetCore\"\r\n"; + private const string ProxyAuthenticateDigestHeader = "Proxy-Authenticate: Digest realm=\"NetCore\", nonce=\"PwOnWgAAAAAAjnbW438AAJSQi1kAAAAA\", qop=\"auth\", stale=false\r\n"; + private const string ProxyAuthenticateNtlmHeader = "Proxy-Authenticate: NTLM\r\n"; + private const string ProxyAuthenticateNegotiateHeader = "Proxy-Authenticate: Negotiate\r\n"; + + private const string ViaHeaderValue = "HTTP/1.1 LoopbackProxyServer"; + + private readonly Socket _listener; + private readonly Uri _uri; + private readonly AuthenticationSchemes _authSchemes; + private readonly bool _connectionCloseAfter407; + private readonly bool _addViaRequestHeader; + private readonly ManualResetEvent _serverStopped; + private readonly List _requests; + private readonly ILogger _logger; + private int _connections; + private bool _disposed; + + public int Connections => _connections; + public List Requests => _requests; + public Uri Uri => _uri; + + private LoopbackProxyServer(Options options, ILogger logger) { - private const string ProxyAuthorizationHeader = "Proxy-Authorization"; - private const string ProxyAuthenticateBasicHeader = "Proxy-Authenticate: Basic realm=\"NetCore\"\r\n"; - private const string ProxyAuthenticateDigestHeader = "Proxy-Authenticate: Digest realm=\"NetCore\", nonce=\"PwOnWgAAAAAAjnbW438AAJSQi1kAAAAA\", qop=\"auth\", stale=false\r\n"; - private const string ProxyAuthenticateNtlmHeader = "Proxy-Authenticate: NTLM\r\n"; - private const string ProxyAuthenticateNegotiateHeader = "Proxy-Authenticate: Negotiate\r\n"; - - private const string ViaHeaderValue = "HTTP/1.1 LoopbackProxyServer"; - - private readonly Socket _listener; - private readonly Uri _uri; - private readonly AuthenticationSchemes _authSchemes; - private readonly bool _connectionCloseAfter407; - private readonly bool _addViaRequestHeader; - private readonly ManualResetEvent _serverStopped; - private readonly List _requests; - private readonly ILogger _logger; - private int _connections; - private bool _disposed; - - public int Connections => _connections; - public List Requests => _requests; - public Uri Uri => _uri; - - private LoopbackProxyServer(Options options, ILogger logger) - { - _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - _listener.Listen(int.MaxValue); - - var ep = (IPEndPoint)_listener.LocalEndPoint; - _uri = new Uri($"http://{ep.Address}:{ep.Port}/"); - _authSchemes = options.AuthenticationSchemes; - _connectionCloseAfter407 = options.ConnectionCloseAfter407; - _addViaRequestHeader = options.AddViaRequestHeader; - _serverStopped = new ManualResetEvent(false); - - _requests = new List(); - _logger = logger; - } - - private void Start() - { - Task.Run(async () => - { - var activeTasks = new ConcurrentDictionary(); - - try - { - while (true) - { - Socket s = await _listener.AcceptAsync().ConfigureAwait(false); - - var connectionTask = Task.Run(async () => - { - try - { - await ProcessConnection(s).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "TestAncillaryError"); - } - }); - - activeTasks.TryAdd(connectionTask, 0); - _ = connectionTask.ContinueWith(t => activeTasks.TryRemove(connectionTask, out _), TaskContinuationOptions.ExecuteSynchronously); - } - } - catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) - { - // caused during Dispose() to cancel the loop. ignore. - } - catch (Exception ex) - { - _logger.LogError(ex, "TestAncillaryError"); - } - - try - { - await Task.WhenAll(activeTasks.Keys).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "TestAncillaryError"); - } - - _serverStopped.Set(); - }); - } + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + _listener.Listen(int.MaxValue); + + var ep = (IPEndPoint)_listener.LocalEndPoint; + _uri = new Uri($"http://{ep.Address}:{ep.Port}/"); + _authSchemes = options.AuthenticationSchemes; + _connectionCloseAfter407 = options.ConnectionCloseAfter407; + _addViaRequestHeader = options.AddViaRequestHeader; + _serverStopped = new ManualResetEvent(false); + + _requests = new List(); + _logger = logger; + } - private async Task ProcessConnection(Socket s) + private void Start() + { + Task.Run(async () => { - Interlocked.Increment(ref _connections); + var activeTasks = new ConcurrentDictionary(); - using (var ns = new NetworkStream(s, ownsSocket: true)) - using (var reader = new StreamReader(ns)) - using (var writer = new StreamWriter(ns) { AutoFlush = true }) + try { while (true) { - if (!await ProcessRequest(s, reader, writer).ConfigureAwait(false)) + Socket s = await _listener.AcceptAsync().ConfigureAwait(false); + + var connectionTask = Task.Run(async () => { - break; - } + try + { + await ProcessConnection(s).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "TestAncillaryError"); + } + }); + + activeTasks.TryAdd(connectionTask, 0); + _ = connectionTask.ContinueWith(t => activeTasks.TryRemove(connectionTask, out _), TaskContinuationOptions.ExecuteSynchronously); } } - } - - private async Task ProcessRequest(Socket clientSocket, StreamReader reader, StreamWriter writer) - { - var headers = new Dictionary(); - string url = null; - string method = null; - string line = null; - - line = reader.ReadLine(); - if (line == null) + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) { - // Connection has been closed by client. - return false; + // caused during Dispose() to cancel the loop. ignore. } - - var request = new ReceivedRequest(); - _requests.Add(request); - - request.RequestLine = line; - string[] requestTokens = request.RequestLine.Split(' '); - method = requestTokens[0]; - url = requestTokens[1]; - while (!string.IsNullOrEmpty(line = reader.ReadLine())) + catch (Exception ex) { - string[] headerParts = line.Split(':'); - headers.Add(headerParts[0].Trim(), headerParts[1].Trim()); + _logger.LogError(ex, "TestAncillaryError"); } - // Store any authentication header and check if credentials are required. - string authValue; - if (headers.TryGetValue(ProxyAuthorizationHeader, out authValue)) + try { - string[] authTokens = authValue.Split(' '); - request.AuthorizationHeaderValueScheme = authTokens[0]; - if (authTokens.Length > 1) - { - request.AuthorizationHeaderValueToken = authTokens[1]; - } + await Task.WhenAll(activeTasks.Keys).ConfigureAwait(false); } - else if (_authSchemes != AuthenticationSchemes.None) + catch (Exception ex) { - Send407Response(writer); - return !_connectionCloseAfter407; + _logger.LogError(ex, "TestAncillaryError"); } - // Handle methods. - if (method.Equals("CONNECT")) - { - string[] tokens = url.Split(':'); - string remoteHost = tokens[0]; - int remotePort = int.Parse(tokens[1]); - - Send200Response(writer); - await ProcessConnectMethod(clientSocket, (NetworkStream)reader.BaseStream, remoteHost, remotePort).ConfigureAwait(false); + _serverStopped.Set(); + }); + } - return false; // connection can't be used for any more requests - } + private async Task ProcessConnection(Socket s) + { + Interlocked.Increment(ref _connections); - // Forward the request to the server. - var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); - foreach (var header in headers) + using (var ns = new NetworkStream(s, ownsSocket: true)) + using (var reader = new StreamReader(ns)) + using (var writer = new StreamWriter(ns) { AutoFlush = true }) + { + while (true) { - if (header.Key != ProxyAuthorizationHeader) // don't forward proxy auth to server + if (!await ProcessRequest(s, reader, writer).ConfigureAwait(false)) { - requestMessage.Headers.Add(header.Key, header.Value); + break; } } + } + } - // Add 'Via' header. - if (_addViaRequestHeader) - { - requestMessage.Headers.Add("Via", ViaHeaderValue); - } + private async Task ProcessRequest(Socket clientSocket, StreamReader reader, StreamWriter writer) + { + var headers = new Dictionary(); + string url = null; + string method = null; + string line = null; - var handler = new HttpClientHandler() { UseProxy = false }; - using (HttpClient outboundClient = new HttpClient(handler)) - using (HttpResponseMessage response = await outboundClient.SendAsync(requestMessage).ConfigureAwait(false)) - { - // Transfer the response headers from the server to the client. - var sb = new StringBuilder($"HTTP/{response.Version.ToString(2)} {(int)response.StatusCode} {response.ReasonPhrase}\r\n"); - foreach (KeyValuePair> header in response.Headers) - { - sb.Append($"{header.Key}: {string.Join(", ", header.Value)}\r\n"); - } - foreach (KeyValuePair> header in response.Content.Headers) - { - sb.Append($"{header.Key}: {string.Join(", ", header.Value)}\r\n"); - } - sb.Append("\r\n"); - writer.Write(sb.ToString()); + line = reader.ReadLine(); + if (line == null) + { + // Connection has been closed by client. + return false; + } - // Forward the response body from the server to the client. - string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - writer.Write(responseBody); + var request = new ReceivedRequest(); + _requests.Add(request); - return true; + request.RequestLine = line; + string[] requestTokens = request.RequestLine.Split(' '); + method = requestTokens[0]; + url = requestTokens[1]; + while (!string.IsNullOrEmpty(line = reader.ReadLine())) + { + string[] headerParts = line.Split(':'); + headers.Add(headerParts[0].Trim(), headerParts[1].Trim()); + } + + // Store any authentication header and check if credentials are required. + string authValue; + if (headers.TryGetValue(ProxyAuthorizationHeader, out authValue)) + { + string[] authTokens = authValue.Split(' '); + request.AuthorizationHeaderValueScheme = authTokens[0]; + if (authTokens.Length > 1) + { + request.AuthorizationHeaderValueToken = authTokens[1]; } } + else if (_authSchemes != AuthenticationSchemes.None) + { + Send407Response(writer); + return !_connectionCloseAfter407; + } - private async Task ProcessConnectMethod(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort) + // Handle methods. + if (method.Equals("CONNECT")) { - // Open connection to destination server. - using Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - await serverSocket.ConnectAsync(remoteHost, remotePort).ConfigureAwait(false); - NetworkStream serverStream = new NetworkStream(serverSocket); + string[] tokens = url.Split(':'); + string remoteHost = tokens[0]; + int remotePort = int.Parse(tokens[1], CultureInfo.InvariantCulture); - // Relay traffic to/from client and destination server. - Task clientCopyTask = Task.Run(async () => - { - try - { - await clientStream.CopyToAsync(serverStream).ConfigureAwait(false); - serverSocket.Shutdown(SocketShutdown.Send); - } - catch (Exception ex) - { - HandleExceptions(ex); - } - }); + Send200Response(writer); + await ProcessConnectMethod(clientSocket, (NetworkStream)reader.BaseStream, remoteHost, remotePort).ConfigureAwait(false); - Task serverCopyTask = Task.Run(async () => + return false; // connection can't be used for any more requests + } + + // Forward the request to the server. + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + foreach (var header in headers) + { + if (header.Key != ProxyAuthorizationHeader) // don't forward proxy auth to server { - try - { - await serverStream.CopyToAsync(clientStream).ConfigureAwait(false); - clientSocket.Shutdown(SocketShutdown.Send); - } - catch (Exception ex) - { - HandleExceptions(ex); - } - }); + requestMessage.Headers.Add(header.Key, header.Value); + } + } - await Task.WhenAll(new[] { clientCopyTask, serverCopyTask }).ConfigureAwait(false); + // Add 'Via' header. + if (_addViaRequestHeader) + { + requestMessage.Headers.Add("Via", ViaHeaderValue); + } - /// Closes sockets to cause both tasks to end, and eats connection reset/aborted errors. - void HandleExceptions(Exception ex) + var handler = new HttpClientHandler() { UseProxy = false }; + using (HttpClient outboundClient = new HttpClient(handler)) + using (HttpResponseMessage response = await outboundClient.SendAsync(requestMessage).ConfigureAwait(false)) + { + // Transfer the response headers from the server to the client. + var sb = new StringBuilder($"HTTP/{response.Version.ToString(2)} {(int)response.StatusCode} {response.ReasonPhrase}\r\n"); + foreach (KeyValuePair> header in response.Headers) + { + var s = $"{header.Key}: {string.Join(", ", header.Value)}\r\n"; + sb.Append(s); + } + foreach (KeyValuePair> header in response.Content.Headers) { - SocketError sockErr = (ex.InnerException as SocketException)?.SocketErrorCode ?? SocketError.Success; + var s = $"{header.Key}: {string.Join(", ", header.Value)}\r\n"; + sb.Append(s); + } + sb.Append("\r\n"); + writer.Write(sb.ToString()); - // If aborted, the other task failed and is asking this task to end. - if (sockErr == SocketError.OperationAborted) - { - return; - } + // Forward the response body from the server to the client. + string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + writer.Write(responseBody); - // Ask the other task to end by disposing, causing OperationAborted. - try - { - clientSocket.Close(); - } - catch (ObjectDisposedException) - { - } + return true; + } + } - try - { - serverSocket.Close(); - } - catch (ObjectDisposedException) - { - } + private async Task ProcessConnectMethod(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort) + { + // Open connection to destination server. + using Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await serverSocket.ConnectAsync(remoteHost, remotePort).ConfigureAwait(false); + NetworkStream serverStream = new NetworkStream(serverSocket); - // Eat reset/abort. - if (sockErr != SocketError.ConnectionReset && sockErr != SocketError.ConnectionAborted) - { - ExceptionDispatchInfo.Capture(ex).Throw(); - } + // Relay traffic to/from client and destination server. + Task clientCopyTask = Task.Run(async () => + { + try + { + await clientStream.CopyToAsync(serverStream).ConfigureAwait(false); + serverSocket.Shutdown(SocketShutdown.Send); } - } + catch (Exception ex) + { + HandleExceptions(ex); + } + }); - private void Send200Response(StreamWriter writer) + Task serverCopyTask = Task.Run(async () => { - string response = - "HTTP/1.1 200 OK\r\n" + - $"Date: {DateTimeOffset.UtcNow:R}\r\n" + - "Content-Length: 0\r\n\r\n"; - writer.Write(response); - } + try + { + await serverStream.CopyToAsync(clientStream).ConfigureAwait(false); + clientSocket.Shutdown(SocketShutdown.Send); + } + catch (Exception ex) + { + HandleExceptions(ex); + } + }); + + await Task.WhenAll(new[] { clientCopyTask, serverCopyTask }).ConfigureAwait(false); - private void Send407Response(StreamWriter writer) + /// Closes sockets to cause both tasks to end, and eats connection reset/aborted errors. + void HandleExceptions(Exception ex) { - string proxyAuthenticateHeaders = string.Empty; - if ((_authSchemes & AuthenticationSchemes.Basic) != 0) + SocketError sockErr = (ex.InnerException as SocketException)?.SocketErrorCode ?? SocketError.Success; + + // If aborted, the other task failed and is asking this task to end. + if (sockErr == SocketError.OperationAborted) { - proxyAuthenticateHeaders += ProxyAuthenticateBasicHeader; + return; } - if ((_authSchemes & AuthenticationSchemes.Digest) != 0) + // Ask the other task to end by disposing, causing OperationAborted. + try { - proxyAuthenticateHeaders += ProxyAuthenticateDigestHeader; + clientSocket.Close(); } - - if ((_authSchemes & AuthenticationSchemes.Ntlm) != 0) + catch (ObjectDisposedException) { - proxyAuthenticateHeaders += ProxyAuthenticateNtlmHeader; } - if ((_authSchemes & AuthenticationSchemes.Negotiate) != 0) + try + { + serverSocket.Close(); + } + catch (ObjectDisposedException) { - proxyAuthenticateHeaders += ProxyAuthenticateNegotiateHeader; } - string response = - "HTTP/1.1 407 Proxy Authentication Required\r\n" + - $"Date: {DateTimeOffset.UtcNow:R}\r\n" + - proxyAuthenticateHeaders + - (_connectionCloseAfter407 ? "Connection: close\r\n" : "") + - "Content-Length: 0\r\n\r\n"; - writer.Write(response); + // Eat reset/abort. + if (sockErr != SocketError.ConnectionReset && sockErr != SocketError.ConnectionAborted) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } } + } - public static LoopbackProxyServer Create(Options options = null, ILogger logger = null) - { - options = options ?? new Options(); - - var server = new LoopbackProxyServer(options, logger ?? NullLogger.Instance); - server.Start(); + private void Send200Response(StreamWriter writer) + { + string response = + "HTTP/1.1 200 OK\r\n" + + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + + "Content-Length: 0\r\n\r\n"; + writer.Write(response); + } - return server; + private void Send407Response(StreamWriter writer) + { + string proxyAuthenticateHeaders = string.Empty; + if ((_authSchemes & AuthenticationSchemes.Basic) != 0) + { + proxyAuthenticateHeaders += ProxyAuthenticateBasicHeader; } - public void Dispose() + if ((_authSchemes & AuthenticationSchemes.Digest) != 0) { - if (!_disposed) - { - _listener.Dispose(); - _serverStopped.WaitOne(); - _disposed = true; - } + proxyAuthenticateHeaders += ProxyAuthenticateDigestHeader; } - public string ViaHeader => ViaHeaderValue; + if ((_authSchemes & AuthenticationSchemes.Ntlm) != 0) + { + proxyAuthenticateHeaders += ProxyAuthenticateNtlmHeader; + } - public class ReceivedRequest + if ((_authSchemes & AuthenticationSchemes.Negotiate) != 0) { - public string RequestLine { get; set; } - public string AuthorizationHeaderValueScheme { get; set; } - public string AuthorizationHeaderValueToken { get; set; } + proxyAuthenticateHeaders += ProxyAuthenticateNegotiateHeader; } - public class Options + string response = + "HTTP/1.1 407 Proxy Authentication Required\r\n" + + $"Date: {DateTimeOffset.UtcNow:R}\r\n" + + proxyAuthenticateHeaders + + (_connectionCloseAfter407 ? "Connection: close\r\n" : "") + + "Content-Length: 0\r\n\r\n"; + writer.Write(response); + } + + public static LoopbackProxyServer Create(Options options = null, ILogger logger = null) + { + options = options ?? new Options(); + + var server = new LoopbackProxyServer(options, logger ?? NullLogger.Instance); + server.Start(); + + return server; + } + + public void Dispose() + { + if (!_disposed) { - public AuthenticationSchemes AuthenticationSchemes { get; set; } = AuthenticationSchemes.None; - public bool ConnectionCloseAfter407 { get; set; } = false; - public bool AddViaRequestHeader { get; set; } = false; + _listener.Dispose(); + _serverStopped.WaitOne(); + _disposed = true; } } + + public string ViaHeader => ViaHeaderValue; + + public class ReceivedRequest + { + public string RequestLine { get; set; } + public string AuthorizationHeaderValueScheme { get; set; } + public string AuthorizationHeaderValueToken { get; set; } + } + + public class Options + { + public AuthenticationSchemes AuthenticationSchemes { get; set; } = AuthenticationSchemes.None; + public bool ConnectionCloseAfter407 { get; set; } = false; + public bool AddViaRequestHeader { get; set; } = false; + } } diff --git a/test/Grpc.Net.ClientFactory.Tests/GrpcHttpClientBuilderExtensionsTests.cs b/test/Grpc.Net.ClientFactory.Tests/GrpcHttpClientBuilderExtensionsTests.cs index 8b8110965..e2fb98c78 100644 --- a/test/Grpc.Net.ClientFactory.Tests/GrpcHttpClientBuilderExtensionsTests.cs +++ b/test/Grpc.Net.ClientFactory.Tests/GrpcHttpClientBuilderExtensionsTests.cs @@ -16,6 +16,7 @@ #endregion +using System.Globalization; using System.Net; using Greet; using Grpc.Core; @@ -498,7 +499,7 @@ public async Task AddCallCredentials_ServiceProvider_RunInScope() var services = new ServiceCollection(); services - .AddScoped(s => new AuthProvider((scopeCount++).ToString())) + .AddScoped(s => new AuthProvider((scopeCount++).ToString(CultureInfo.InvariantCulture))) .AddGrpcClient(o => { o.Address = new Uri("https://localhost"); diff --git a/test/dotnet-grpc.Tests/CommandBaseTests.cs b/test/dotnet-grpc.Tests/CommandBaseTests.cs index 6ac2e35b9..6249bf167 100644 --- a/test/dotnet-grpc.Tests/CommandBaseTests.cs +++ b/test/dotnet-grpc.Tests/CommandBaseTests.cs @@ -267,7 +267,6 @@ public void AddProtobufReference_AdditionalImportDirs(string additionalImportDir Assert.False(protoRef.HasMetadata(CommandBase.LinkElement)); } - [Test] public void AddProtobufReference_Without_AdditionalImportDirs() {