Skip to content

Commit

Permalink
Allow GrpcChannel to use WinHttpHandler on Windows Server 2019 (#2362)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Jan 18, 2024
1 parent 0cad749 commit 3103c69
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 22 deletions.
40 changes: 29 additions & 11 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 " +
Expand All @@ -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>? callCredentials)
{
if (channelOptions.Credentials != null)
Expand Down
16 changes: 9 additions & 7 deletions src/Grpc.Net.Client/Internal/NtDll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

#endregion

#if !NET5_0_OR_GREATER

using System.Runtime.InteropServices;

namespace Grpc.Net.Client.Internal;
Expand All @@ -27,19 +25,25 @@ namespace Grpc.Net.Client.Internal;
/// </summary>
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<OSVERSIONINFOEX>() };

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
Expand Down Expand Up @@ -68,5 +72,3 @@ internal struct OSVERSIONINFOEX
public byte Reserved;
}
}

#endif
29 changes: 28 additions & 1 deletion src/Grpc.Net.Client/Internal/OperatingSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@ internal interface IOperatingSystem
bool IsBrowser { get; }
bool IsAndroid { get; }
bool IsWindows { get; }
bool IsWindowsServer { get; }
Version OSVersion { get; }
}

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

private readonly Lazy<bool> _isWindowsServer;

public bool IsBrowser { get; }
public bool IsAndroid { get; }
public bool IsWindows { get; }
public bool IsWindowsServer => _isWindowsServer.Value;
public Version OSVersion { get; }

private OperatingSystem()
Expand All @@ -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<bool>(() =>
{
if (IsWindows)
{
NtDll.DetectWindowsVersion(out _, out var isWindowsServer);
return isWindowsServer;
}

return false;
}, LazyThreadSafetyMode.ExecutionAndPublication);
#else
IsAndroid = false;
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
Expand All @@ -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<bool>(() => windowsServer, LazyThreadSafetyMode.ExecutionAndPublication);
}
else
{
OSVersion = Environment.OSVersion.Version;
_isWindowsServer = new Lazy<bool>(() => false, LazyThreadSafetyMode.ExecutionAndPublication);
}
#endif
}
}
1 change: 1 addition & 0 deletions test/Grpc.Net.Client.Tests/GetStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions test/Grpc.Net.Client.Tests/GrpcChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
19 changes: 16 additions & 3 deletions test/Grpc.Net.Client.Tests/OperatingSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 3103c69

Please sign in to comment.