Skip to content

Commit 3103c69

Browse files
authored
Allow GrpcChannel to use WinHttpHandler on Windows Server 2019 (#2362)
1 parent 0cad749 commit 3103c69

File tree

6 files changed

+84
-22
lines changed

6 files changed

+84
-22
lines changed

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,9 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
185185
Log.AddressPathUnused(Logger, Address.OriginalString);
186186
}
187187

188-
// Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly.
189-
// This scenario is supported in these versions of Windows or later:
190-
// -Windows Server 2022 has partial support.
191-
// -Unary and server streaming methods are supported.
192-
// -Client and bidi streaming methods aren't supported.
193-
// -Windows 11 has full support.
194-
//
195-
// GrpcChannel validates the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022.
196-
// Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022.
197-
const int WinServer2022BuildVersion = 20348;
198188
if (HttpHandlerType == HttpHandlerType.WinHttpHandler &&
199189
OperatingSystem.IsWindows &&
200-
OperatingSystem.OSVersion.Build < WinServer2022BuildVersion)
190+
!ValidateWinHttpHandlerOperatingSystemVersion())
201191
{
202192
throw new InvalidOperationException("The channel configuration isn't valid on this operating system. " +
203193
"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
206196
}
207197
}
208198

199+
private bool ValidateWinHttpHandlerOperatingSystemVersion()
200+
{
201+
// Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly.
202+
// This scenario is supported in these versions of Windows or later:
203+
// -Windows Server 2019 and Windows Server 2022 have partial support.
204+
// -Unary and server streaming methods are supported.
205+
// -Client and bidi streaming methods aren't supported.
206+
// -Windows 11 has full support.
207+
const int WinServer2022BuildVersion = 20348;
208+
const int WinServer2019BuildVersion = 17763;
209+
210+
// Validate the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022.
211+
// Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022.
212+
if (OperatingSystem.OSVersion.Build >= WinServer2022BuildVersion)
213+
{
214+
return true;
215+
}
216+
217+
// Validate the Windows version is WinServer2019. Its build numbers are mixed with Windows 10, so we must check
218+
// the OS version is Windows Server and the build number together to avoid allowing Windows 10.
219+
if (OperatingSystem.IsWindowsServer && OperatingSystem.OSVersion.Build >= WinServer2019BuildVersion)
220+
{
221+
return true;
222+
}
223+
224+
return false;
225+
}
226+
209227
private void ResolveCredentials(GrpcChannelOptions channelOptions, out bool isSecure, out List<CallCredentials>? callCredentials)
210228
{
211229
if (channelOptions.Credentials != null)

src/Grpc.Net.Client/Internal/NtDll.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
#endregion
1818

19-
#if !NET5_0_OR_GREATER
20-
2119
using System.Runtime.InteropServices;
2220

2321
namespace Grpc.Net.Client.Internal;
@@ -27,19 +25,25 @@ namespace Grpc.Net.Client.Internal;
2725
/// </summary>
2826
internal static class NtDll
2927
{
28+
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
3029
[DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
3130
internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
31+
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
3232

33-
internal static Version DetectWindowsVersion()
33+
internal static void DetectWindowsVersion(out Version version, out bool isWindowsServer)
3434
{
35-
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
35+
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa
36+
const byte VER_NT_SERVER = 3;
37+
38+
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf<OSVERSIONINFOEX>() };
3639

3740
if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS)
3841
{
3942
throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}.");
4043
}
4144

42-
return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
45+
version = new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
46+
isWindowsServer = osVersionInfo.ProductType == VER_NT_SERVER;
4347
}
4448

4549
internal enum NTSTATUS : uint
@@ -68,5 +72,3 @@ internal struct OSVERSIONINFOEX
6872
public byte Reserved;
6973
}
7074
}
71-
72-
#endif

src/Grpc.Net.Client/Internal/OperatingSystem.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@ internal interface IOperatingSystem
2525
bool IsBrowser { get; }
2626
bool IsAndroid { get; }
2727
bool IsWindows { get; }
28+
bool IsWindowsServer { get; }
2829
Version OSVersion { get; }
2930
}
3031

3132
internal sealed class OperatingSystem : IOperatingSystem
3233
{
3334
public static readonly OperatingSystem Instance = new OperatingSystem();
3435

36+
private readonly Lazy<bool> _isWindowsServer;
37+
3538
public bool IsBrowser { get; }
3639
public bool IsAndroid { get; }
3740
public bool IsWindows { get; }
41+
public bool IsWindowsServer => _isWindowsServer.Value;
3842
public Version OSVersion { get; }
3943

4044
private OperatingSystem()
@@ -44,6 +48,19 @@ private OperatingSystem()
4448
IsWindows = System.OperatingSystem.IsWindows();
4549
IsBrowser = System.OperatingSystem.IsBrowser();
4650
OSVersion = Environment.OSVersion.Version;
51+
52+
// Windows Server detection requires a P/Invoke call to RtlGetVersion.
53+
// Get the value lazily so that it is only called if needed.
54+
_isWindowsServer = new Lazy<bool>(() =>
55+
{
56+
if (IsWindows)
57+
{
58+
NtDll.DetectWindowsVersion(out _, out var isWindowsServer);
59+
return isWindowsServer;
60+
}
61+
62+
return false;
63+
}, LazyThreadSafetyMode.ExecutionAndPublication);
4764
#else
4865
IsAndroid = false;
4966
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
@@ -55,7 +72,17 @@ private OperatingSystem()
5572
//
5673
// Get correct Windows version directly from Windows by calling RtlGetVersion.
5774
// https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
58-
OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version;
75+
if (IsWindows)
76+
{
77+
NtDll.DetectWindowsVersion(out var windowsVersion, out var windowsServer);
78+
OSVersion = windowsVersion;
79+
_isWindowsServer = new Lazy<bool>(() => windowsServer, LazyThreadSafetyMode.ExecutionAndPublication);
80+
}
81+
else
82+
{
83+
OSVersion = Environment.OSVersion.Version;
84+
_isWindowsServer = new Lazy<bool>(() => false, LazyThreadSafetyMode.ExecutionAndPublication);
85+
}
5986
#endif
6087
}
6188
}

test/Grpc.Net.Client.Tests/GetStatusTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ private class TestOperatingSystem : IOperatingSystem
215215
public bool IsBrowser { get; set; }
216216
public bool IsAndroid { get; set; }
217217
public bool IsWindows { get; set; }
218+
public bool IsWindowsServer { get; }
218219
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
219220
}
220221

test/Grpc.Net.Client.Tests/GrpcChannelTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ private class TestOperatingSystem : IOperatingSystem
614614
public bool IsBrowser { get; set; }
615615
public bool IsAndroid { get; set; }
616616
public bool IsWindows { get; set; }
617+
public bool IsWindowsServer { get; }
617618
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
618619
}
619620

test/Grpc.Net.Client.Tests/OperatingSystemTests.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,28 @@ public class OperatingSystemTests
2929
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
3030
public void DetectWindowsVersion_Windows_MatchesEnvironment()
3131
{
32-
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
33-
Assert.AreEqual(Environment.OSVersion.Version, NtDll.DetectWindowsVersion());
32+
NtDll.DetectWindowsVersion(out var version, out _);
33+
34+
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting.
35+
Assert.AreEqual(Environment.OSVersion.Version, version);
36+
}
37+
38+
[Test]
39+
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
40+
public void InstanceAndIsWindowsServer_Windows_MatchesEnvironment()
41+
{
42+
NtDll.DetectWindowsVersion(out var version, out var isWindowsServer);
43+
44+
Assert.AreEqual(true, OperatingSystem.Instance.IsWindows);
45+
Assert.AreEqual(version, OperatingSystem.Instance.OSVersion);
46+
Assert.AreEqual(isWindowsServer, OperatingSystem.Instance.IsWindowsServer);
3447
}
3548
#endif
3649

3750
[Test]
3851
public void OSVersion_ModernDotNet_MatchesEnvironment()
3952
{
40-
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
53+
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting.
4154
Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion);
4255
}
4356
}

0 commit comments

Comments
 (0)