From 3103c6937fc183fab4102f40f17e6eba7bdff322 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 18 Jan 2024 08:05:06 +0800 Subject: [PATCH] Allow GrpcChannel to use WinHttpHandler on Windows Server 2019 (#2362) --- src/Grpc.Net.Client/GrpcChannel.cs | 40 ++++++++++++++----- src/Grpc.Net.Client/Internal/NtDll.cs | 16 ++++---- .../Internal/OperatingSystem.cs | 29 +++++++++++++- test/Grpc.Net.Client.Tests/GetStatusTests.cs | 1 + .../Grpc.Net.Client.Tests/GrpcChannelTests.cs | 1 + .../OperatingSystemTests.cs | 19 +++++++-- 6 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/Grpc.Net.Client/GrpcChannel.cs b/src/Grpc.Net.Client/GrpcChannel.cs index 47a782ad0..a65ce166b 100644 --- a/src/Grpc.Net.Client/GrpcChannel.cs +++ b/src/Grpc.Net.Client/GrpcChannel.cs @@ -185,19 +185,9 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr Log.AddressPathUnused(Logger, Address.OriginalString); } - // Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly. - // This scenario is supported in these versions of Windows or later: - // -Windows Server 2022 has partial support. - // -Unary and server streaming methods are supported. - // -Client and bidi streaming methods aren't supported. - // -Windows 11 has full support. - // - // GrpcChannel validates the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022. - // Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022. - const int WinServer2022BuildVersion = 20348; if (HttpHandlerType == HttpHandlerType.WinHttpHandler && OperatingSystem.IsWindows && - OperatingSystem.OSVersion.Build < WinServer2022BuildVersion) + !ValidateWinHttpHandlerOperatingSystemVersion()) { throw new InvalidOperationException("The channel configuration isn't valid on this operating system. " + "The channel is configured to use WinHttpHandler and the current version of Windows " + @@ -206,6 +196,34 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr } } + private bool ValidateWinHttpHandlerOperatingSystemVersion() + { + // Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly. + // This scenario is supported in these versions of Windows or later: + // -Windows Server 2019 and Windows Server 2022 have partial support. + // -Unary and server streaming methods are supported. + // -Client and bidi streaming methods aren't supported. + // -Windows 11 has full support. + const int WinServer2022BuildVersion = 20348; + const int WinServer2019BuildVersion = 17763; + + // Validate the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022. + // Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022. + if (OperatingSystem.OSVersion.Build >= WinServer2022BuildVersion) + { + return true; + } + + // Validate the Windows version is WinServer2019. Its build numbers are mixed with Windows 10, so we must check + // the OS version is Windows Server and the build number together to avoid allowing Windows 10. + if (OperatingSystem.IsWindowsServer && OperatingSystem.OSVersion.Build >= WinServer2019BuildVersion) + { + return true; + } + + return false; + } + private void ResolveCredentials(GrpcChannelOptions channelOptions, out bool isSecure, out List? callCredentials) { if (channelOptions.Credentials != null) diff --git a/src/Grpc.Net.Client/Internal/NtDll.cs b/src/Grpc.Net.Client/Internal/NtDll.cs index 2abdbf5ad..3f5c6a1df 100644 --- a/src/Grpc.Net.Client/Internal/NtDll.cs +++ b/src/Grpc.Net.Client/Internal/NtDll.cs @@ -16,8 +16,6 @@ #endregion -#if !NET5_0_OR_GREATER - using System.Runtime.InteropServices; namespace Grpc.Net.Client.Internal; @@ -27,19 +25,25 @@ namespace Grpc.Net.Client.Internal; /// internal static class NtDll { +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)] internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo); +#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time - internal static Version DetectWindowsVersion() + internal static void DetectWindowsVersion(out Version version, out bool isWindowsServer) { - var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) }; + // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa + const byte VER_NT_SERVER = 3; + + var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf() }; if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS) { throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}."); } - return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0); + version = new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0); + isWindowsServer = osVersionInfo.ProductType == VER_NT_SERVER; } internal enum NTSTATUS : uint @@ -68,5 +72,3 @@ internal struct OSVERSIONINFOEX public byte Reserved; } } - -#endif diff --git a/src/Grpc.Net.Client/Internal/OperatingSystem.cs b/src/Grpc.Net.Client/Internal/OperatingSystem.cs index 4d2e45c73..1ce1c6a5a 100644 --- a/src/Grpc.Net.Client/Internal/OperatingSystem.cs +++ b/src/Grpc.Net.Client/Internal/OperatingSystem.cs @@ -25,6 +25,7 @@ internal interface IOperatingSystem bool IsBrowser { get; } bool IsAndroid { get; } bool IsWindows { get; } + bool IsWindowsServer { get; } Version OSVersion { get; } } @@ -32,9 +33,12 @@ internal sealed class OperatingSystem : IOperatingSystem { public static readonly OperatingSystem Instance = new OperatingSystem(); + private readonly Lazy _isWindowsServer; + public bool IsBrowser { get; } public bool IsAndroid { get; } public bool IsWindows { get; } + public bool IsWindowsServer => _isWindowsServer.Value; public Version OSVersion { get; } private OperatingSystem() @@ -44,6 +48,19 @@ private OperatingSystem() IsWindows = System.OperatingSystem.IsWindows(); IsBrowser = System.OperatingSystem.IsBrowser(); OSVersion = Environment.OSVersion.Version; + + // Windows Server detection requires a P/Invoke call to RtlGetVersion. + // Get the value lazily so that it is only called if needed. + _isWindowsServer = new Lazy(() => + { + if (IsWindows) + { + NtDll.DetectWindowsVersion(out _, out var isWindowsServer); + return isWindowsServer; + } + + return false; + }, LazyThreadSafetyMode.ExecutionAndPublication); #else IsAndroid = false; IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); @@ -55,7 +72,17 @@ private OperatingSystem() // // Get correct Windows version directly from Windows by calling RtlGetVersion. // https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html - OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version; + if (IsWindows) + { + NtDll.DetectWindowsVersion(out var windowsVersion, out var windowsServer); + OSVersion = windowsVersion; + _isWindowsServer = new Lazy(() => windowsServer, LazyThreadSafetyMode.ExecutionAndPublication); + } + else + { + OSVersion = Environment.OSVersion.Version; + _isWindowsServer = new Lazy(() => false, LazyThreadSafetyMode.ExecutionAndPublication); + } #endif } } diff --git a/test/Grpc.Net.Client.Tests/GetStatusTests.cs b/test/Grpc.Net.Client.Tests/GetStatusTests.cs index 2c0340241..aef7dcf42 100644 --- a/test/Grpc.Net.Client.Tests/GetStatusTests.cs +++ b/test/Grpc.Net.Client.Tests/GetStatusTests.cs @@ -215,6 +215,7 @@ private class TestOperatingSystem : IOperatingSystem public bool IsBrowser { get; set; } public bool IsAndroid { get; set; } public bool IsWindows { get; set; } + public bool IsWindowsServer { get; } public Version OSVersion { get; set; } = new Version(1, 2, 3, 4); } diff --git a/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs b/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs index 15a9abb12..487c5672c 100644 --- a/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs +++ b/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs @@ -614,6 +614,7 @@ private class TestOperatingSystem : IOperatingSystem public bool IsBrowser { get; set; } public bool IsAndroid { get; set; } public bool IsWindows { get; set; } + public bool IsWindowsServer { get; } public Version OSVersion { get; set; } = new Version(1, 2, 3, 4); } diff --git a/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs b/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs index c91c3b6e1..cc2425dd5 100644 --- a/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs +++ b/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs @@ -29,15 +29,28 @@ public class OperatingSystemTests [Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")] public void DetectWindowsVersion_Windows_MatchesEnvironment() { - // It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting. - Assert.AreEqual(Environment.OSVersion.Version, NtDll.DetectWindowsVersion()); + NtDll.DetectWindowsVersion(out var version, out _); + + // It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting. + Assert.AreEqual(Environment.OSVersion.Version, version); + } + + [Test] + [Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")] + public void InstanceAndIsWindowsServer_Windows_MatchesEnvironment() + { + NtDll.DetectWindowsVersion(out var version, out var isWindowsServer); + + Assert.AreEqual(true, OperatingSystem.Instance.IsWindows); + Assert.AreEqual(version, OperatingSystem.Instance.OSVersion); + Assert.AreEqual(isWindowsServer, OperatingSystem.Instance.IsWindowsServer); } #endif [Test] public void OSVersion_ModernDotNet_MatchesEnvironment() { - // It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting. + // It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting. Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion); } }