Skip to content

Commit 400bbb7

Browse files
authored
[2.56.x] Validate Windows version when using WinHttpHandler (#2233)
* Validate Windows version when using WinHttpHandler (#2229) * Improve comment in GrpcChannel for WinHttpHandler + OS validation (#2237) * Update OS version detection to get version directly from Windows (#2239)
1 parent 1422de0 commit 400bbb7

File tree

6 files changed

+216
-3
lines changed

6 files changed

+216
-3
lines changed

src/Grpc.Net.Client/GrpcChannel.cs

+20
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,26 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
184184
{
185185
Log.AddressPathUnused(Logger, Address.OriginalString);
186186
}
187+
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;
198+
if (HttpHandlerType == HttpHandlerType.WinHttpHandler &&
199+
OperatingSystem.IsWindows &&
200+
OperatingSystem.OSVersion.Build < WinServer2022BuildVersion)
201+
{
202+
throw new InvalidOperationException("The channel configuration isn't valid on this operating system. " +
203+
"The channel is configured to use WinHttpHandler and the current version of Windows " +
204+
"doesn't support HTTP/2 features required by gRPC. Windows Server 2022 or Windows 11 or later is required. " +
205+
"For more information, see https://aka.ms/aspnet/grpc/netframework.");
206+
}
187207
}
188208

189209
private void ResolveCredentials(GrpcChannelOptions channelOptions, out bool isSecure, out List<CallCredentials>? callCredentials)

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

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
#if !NET5_0_OR_GREATER
20+
21+
using System.Runtime.InteropServices;
22+
23+
namespace Grpc.Net.Client.Internal;
24+
25+
/// <summary>
26+
/// Types for calling RtlGetVersion. See https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
27+
/// </summary>
28+
internal static class NtDll
29+
{
30+
[DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
31+
internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
32+
33+
internal static Version DetectWindowsVersion()
34+
{
35+
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
36+
37+
if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS)
38+
{
39+
throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}.");
40+
}
41+
42+
return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
43+
}
44+
45+
internal enum NTSTATUS : uint
46+
{
47+
/// <summary>
48+
/// The operation completed successfully.
49+
/// </summary>
50+
STATUS_SUCCESS = 0x00000000
51+
}
52+
53+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
54+
internal struct OSVERSIONINFOEX
55+
{
56+
// The OSVersionInfoSize field must be set to Marshal.SizeOf(typeof(OSVERSIONINFOEX))
57+
public int OSVersionInfoSize;
58+
public int MajorVersion;
59+
public int MinorVersion;
60+
public int BuildNumber;
61+
public int PlatformId;
62+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
63+
public string CSDVersion;
64+
public ushort ServicePackMajor;
65+
public ushort ServicePackMinor;
66+
public short SuiteMask;
67+
public byte ProductType;
68+
public byte Reserved;
69+
}
70+
}
71+
72+
#endif

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

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -24,6 +24,8 @@ internal interface IOperatingSystem
2424
{
2525
bool IsBrowser { get; }
2626
bool IsAndroid { get; }
27+
bool IsWindows { get; }
28+
Version OSVersion { get; }
2729
}
2830

2931
internal sealed class OperatingSystem : IOperatingSystem
@@ -32,14 +34,28 @@ internal sealed class OperatingSystem : IOperatingSystem
3234

3335
public bool IsBrowser { get; }
3436
public bool IsAndroid { get; }
37+
public bool IsWindows { get; }
38+
public Version OSVersion { get; }
3539

3640
private OperatingSystem()
3741
{
38-
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
3942
#if NET5_0_OR_GREATER
4043
IsAndroid = System.OperatingSystem.IsAndroid();
44+
IsWindows = System.OperatingSystem.IsWindows();
45+
IsBrowser = System.OperatingSystem.IsBrowser();
46+
OSVersion = Environment.OSVersion.Version;
4147
#else
4248
IsAndroid = false;
49+
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
50+
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
51+
52+
// Older versions of .NET report an OSVersion.Version based on Windows compatibility settings.
53+
// For example, if an app running on Windows 11 is configured to be "compatible" with Windows 10
54+
// then the version returned is always Windows 10.
55+
//
56+
// Get correct Windows version directly from Windows by calling RtlGetVersion.
57+
// https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
58+
OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version;
4359
#endif
4460
}
4561
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -214,6 +214,8 @@ private class TestOperatingSystem : IOperatingSystem
214214
{
215215
public bool IsBrowser { get; set; }
216216
public bool IsAndroid { get; set; }
217+
public bool IsWindows { get; set; }
218+
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
217219
}
218220

219221
[Test]

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

+60
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,66 @@ private class TestOperatingSystem : IOperatingSystem
613613
{
614614
public bool IsBrowser { get; set; }
615615
public bool IsAndroid { get; set; }
616+
public bool IsWindows { get; set; }
617+
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
618+
}
619+
620+
[Test]
621+
public void WinHttpHandler_UnsupportedWindows_Throw()
622+
{
623+
// Arrange
624+
var services = new ServiceCollection();
625+
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem
626+
{
627+
IsWindows = true,
628+
OSVersion = new Version(1, 2, 3, 4)
629+
});
630+
631+
#pragma warning disable CS0436 // Just need to have a type called WinHttpHandler to activate new behavior.
632+
var winHttpHandler = new WinHttpHandler(new TestHttpMessageHandler());
633+
#pragma warning restore CS0436
634+
635+
// Act
636+
var ex = Assert.Throws<InvalidOperationException>(() =>
637+
{
638+
GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
639+
{
640+
HttpHandler = winHttpHandler,
641+
ServiceProvider = services.BuildServiceProvider()
642+
});
643+
});
644+
645+
// Assert
646+
Assert.AreEqual(ex!.Message, "The channel configuration isn't valid on this operating system. " +
647+
"The channel is configured to use WinHttpHandler and the current version of Windows " +
648+
"doesn't support HTTP/2 features required by gRPC. Windows Server 2022 or Windows 11 or later is required. " +
649+
"For more information, see https://aka.ms/aspnet/grpc/netframework.");
650+
}
651+
652+
[Test]
653+
public void WinHttpHandler_SupportedWindows_Success()
654+
{
655+
// Arrange
656+
var services = new ServiceCollection();
657+
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem
658+
{
659+
IsWindows = true,
660+
OSVersion = Version.Parse("10.0.20348.169")
661+
});
662+
663+
#pragma warning disable CS0436 // Just need to have a type called WinHttpHandler to activate new behavior.
664+
var winHttpHandler = new WinHttpHandler(new TestHttpMessageHandler());
665+
#pragma warning restore CS0436
666+
667+
// Act
668+
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
669+
{
670+
HttpHandler = winHttpHandler,
671+
ServiceProvider = services.BuildServiceProvider()
672+
});
673+
674+
// Assert
675+
Assert.AreEqual(HttpHandlerType.WinHttpHandler, channel.HttpHandlerType);
616676
}
617677

618678
#if SUPPORT_LOAD_BALANCING
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using Grpc.Net.Client.Internal;
20+
using NUnit.Framework;
21+
using OperatingSystem = Grpc.Net.Client.Internal.OperatingSystem;
22+
23+
namespace Grpc.Net.Client.Tests;
24+
25+
public class OperatingSystemTests
26+
{
27+
#if !NET5_0_OR_GREATER
28+
[Test]
29+
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
30+
public void DetectWindowsVersion_Windows_MatchesEnvironment()
31+
{
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());
34+
}
35+
#endif
36+
37+
[Test]
38+
public void OSVersion_ModernDotNet_MatchesEnvironment()
39+
{
40+
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
41+
Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion);
42+
}
43+
}

0 commit comments

Comments
 (0)