diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 38a30def8f02..34e606099902 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -104,15 +104,15 @@ This file should be imported by eng/Versions.props 4.13.0-3.24613.7 4.13.0-3.24613.7 - 9.10.0-preview.1.25462.1 - 9.10.0-preview.1.25462.1 - 9.10.0-preview.1.25462.1 + 9.10.0-preview.1.25468.3 + 9.10.0-preview.1.25468.3 + 9.10.0-preview.1.25468.3 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 - 1.0.0-prerelease.25458.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 + 1.0.0-prerelease.25467.1 17.12.36 17.12.36 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index d2f6a5f99199..298dcd5d1569 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -390,37 +390,37 @@ https://github.com/dotnet/dotnet 2dea164f01d307c409cfe0d0ee5cb8a0691e3c94 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://github.com/dotnet/extensions - d299e16f15234f9808b18fef50bf7770113fb4b2 + 53ef1158f9f42632e111d6873a8cd72b803b4ae6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 - + https://dev.azure.com/dnceng/internal/_git/dotnet-optimization - 373ed5bf1a64c212e655063e58967eb62f9187ef + 59dc6a9bf1b3e3ab71c73d94160c2049fb104cd1 diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs index 6ce14ee0952d..8e2d84f2f11b 100644 --- a/src/Components/Web/src/Virtualization/Virtualize.cs +++ b/src/Components/Web/src/Virtualization/Virtualize.cs @@ -362,6 +362,10 @@ private void CalcualteItemDistribution( _ => MaxItemCount }; + // Count the OverscanCount as used capacity, so we don't end up in a situation where + // the user has set a very low MaxItemCount and we end up in an infinite loading loop. + maxItemCount += OverscanCount * 2; + itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - OverscanCount); visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2 * OverscanCount; unusedItemCapacity = Math.Max(0, visibleItemCapacity - maxItemCount); diff --git a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs index 565dc0190fcd..4fa21ef3fe11 100644 --- a/src/Components/test/E2ETest/Tests/VirtualizationTest.cs +++ b/src/Components/test/E2ETest/Tests/VirtualizationTest.cs @@ -291,14 +291,14 @@ public void CanLimitMaxItemsRendered(bool useAppContext) // we only render 10 items due to the MaxItemCount setting var scrollArea = Browser.Exists(By.Id("virtualize-scroll-area")); var getItems = () => scrollArea.FindElements(By.ClassName("my-item")); - Browser.Equal(10, () => getItems().Count); + Browser.Equal(16, () => getItems().Count); Browser.Equal("Id: 0; Name: Thing 0", () => getItems().First().Text); // Scrolling still works and loads new data, though there's no guarantee about // exactly how many items will show up at any one time Browser.ExecuteJavaScript("document.getElementById('virtualize-scroll-area').scrollTop = 300;"); Browser.NotEqual("Id: 0; Name: Thing 0", () => getItems().First().Text); - Browser.True(() => getItems().Count > 3 && getItems().Count <= 10); + Browser.True(() => getItems().Count > 3 && getItems().Count <= 16); } [Fact] @@ -573,6 +573,101 @@ public void EmptyContentRendered_Async() int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count; } + [Fact] + public void CanElevateEffectiveMaxItemCount_WhenOverscanExceedsMax() + { + Browser.MountTestComponent(); + var container = Browser.Exists(By.Id("virtualize-large-overscan")); + // Ensure we have an initial contiguous batch and the elevated effective max has kicked in (>= OverscanCount) + var indices = GetVisibleItemIndices(); + Browser.True(() => indices.Count >= 200); + + // Give focus so PageDown works + container.Click(); + + var js = (IJavaScriptExecutor)Browser; + var lastMaxIndex = -1; + var lastScrollTop = -1L; + + // Check if we've reached (or effectively reached) the bottom + var scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container); + var clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container); + var scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + while (scrollTop + clientHeight < scrollHeight) + { + // Validate contiguity on the current page + Browser.True(() => IsCurrentViewContiguous(indices)); + + // Track progress in indices + var currentMax = indices.Max(); + Assert.True(currentMax >= lastMaxIndex, $"Unexpected backward movement: previous max {lastMaxIndex}, current max {currentMax}."); + lastMaxIndex = currentMax; + + // Send PageDown + container.SendKeys(Keys.PageDown); + + // Wait for scrollTop to change (progress) to avoid infinite loop + var prevScrollTop = scrollTop; + Browser.True(() => + { + var st = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + if (st > prevScrollTop) + { + lastScrollTop = st; + return true; + } + return false; + }); + scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container); + clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container); + scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container); + } + + // Final contiguous assertion at bottom + Browser.True(() => IsCurrentViewContiguous()); + + // Helper: check visible items contiguous with no holes + bool IsCurrentViewContiguous(List existingIndices = null) + { + var indices = existingIndices ?? GetVisibleItemIndices(); + if (indices.Count == 0) + { + return false; + } + + if (indices[^1] - indices[0] != indices.Count - 1) + { + return false; + } + for (var i = 1; i < indices.Count; i++) + { + if (indices[i] - indices[i - 1] != 1) + { + return false; + } + } + return true; + } + + List GetVisibleItemIndices() + { + var elements = container.FindElements(By.CssSelector(".large-overscan-item")); + var list = new List(elements.Count); + foreach (var el in elements) + { + var text = el.Text; + if (text.StartsWith("Item ", StringComparison.Ordinal)) + { + if (int.TryParse(text.AsSpan(5), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + list.Add(value); + } + } + } + return list; + } + } + private string[] GetPeopleNames(IWebElement container) { var peopleElements = container.FindElements(By.CssSelector(".person span")); diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index a3bc250f0634..d7df87adbeed 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -119,6 +119,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor new file mode 100644 index 000000000000..3beb29cf87c2 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/VirtualizationLargeOverscan.razor @@ -0,0 +1,12 @@ +@* Test component to validate behavior when OverscanCount greatly exceeds MaxItemCount. *@ +@using Microsoft.AspNetCore.Components.Web.Virtualization + +
+ +
Item @context
+
+
+ +@code { + private IList _items = Enumerable.Range(0, 5000).ToList(); +} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index ed61268897ef..1b9e03dc5706 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -30,6 +31,7 @@ { var connectionFeature = context.Features.GetRequiredFeature(); var httpSysPropFeature = context.Features.GetRequiredFeature(); + var tlsHandshakeFeature = context.Features.GetRequiredFeature(); // first time invocation to find out required size var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty(), out var bytesReturned); @@ -41,7 +43,14 @@ success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); Debug.Assert(success); - await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); + await context.Response.WriteAsync( + $""" + connectionId = {connectionFeature.ConnectionId}; + negotiated cipher suite = {tlsHandshakeFeature.NegotiatedCipherSuite}; + tlsClientHello.length = {bytesReturned}; + tlsclienthello start = {string.Join(' ', bytes.AsSpan(0, 30).ToArray())} + """); + await next(context); }); diff --git a/src/Servers/HttpSys/src/LoggerEventIds.cs b/src/Servers/HttpSys/src/LoggerEventIds.cs index e6d745f506be..a9f2c969c6bc 100644 --- a/src/Servers/HttpSys/src/LoggerEventIds.cs +++ b/src/Servers/HttpSys/src/LoggerEventIds.cs @@ -60,4 +60,5 @@ internal static class LoggerEventIds public const int AcceptObserveExpectationMismatch = 53; public const int RequestParsingError = 54; public const int TlsListenerError = 55; + public const int QueryTlsCipherSuiteError = 56; } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index f5dfbc96a6cd..fbada3843642 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -70,6 +70,7 @@ internal static unsafe uint HttpSetRequestProperty(SafeHandle requestQueueHandle internal static bool SupportsReset { get; } internal static bool SupportsDelegation { get; } internal static bool SupportsClientHello { get; } + internal static bool SupportsQueryTlsCipherInfo { get; } internal static bool Supported { get; } static unsafe HttpApi() @@ -86,6 +87,7 @@ static unsafe HttpApi() SupportsTrailers = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureResponseTrailers); SupportsDelegation = IsFeatureSupported(HTTP_FEATURE_ID.HttpFeatureDelegateEx); SupportsClientHello = IsFeatureSupported((HTTP_FEATURE_ID)11 /* HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello */) && HttpGetRequestPropertySupported; + SupportsQueryTlsCipherInfo = IsFeatureSupported((HTTP_FEATURE_ID)15 /* HTTP_FEATURE_ID.HttpFeatureQueryCipherInfo */) && HttpGetRequestPropertySupported; } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 8e4babf7ca21..3d99fe8e718b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Net; +using System.Net.Security; using System.Security; using System.Security.Authentication; using System.Security.Cryptography; @@ -334,6 +335,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint public SslProtocols Protocol { get; private set; } + public TlsCipherSuite? NegotiatedCipherSuite { get; private set; } + [Obsolete(Obsoletions.RuntimeTlsCipherAlgorithmEnumsMessage, DiagnosticId = Obsoletions.RuntimeTlsCipherAlgorithmEnumsDiagId, UrlFormat = Obsoletions.RuntimeSharedUrlFormat)] public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -356,6 +359,8 @@ private void GetTlsHandshakeResults() { var handshake = RequestContext.GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = RequestContext.GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 1c80f92febc2..a66e44d1484c 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO.Pipelines; using System.Net; +using System.Net.Security; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; @@ -593,6 +594,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol; + TlsCipherSuite? ITlsHandshakeFeature.NegotiatedCipherSuite => Request.NegotiatedCipherSuite; + #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs index d7766698bc41..0fce6896778b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.Log.cs @@ -23,5 +23,8 @@ private static partial class Log [LoggerMessage(LoggerEventIds.RequestParsingError, LogLevel.Debug, "Failed to invoke QueryTlsClientHello; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "TlsClientHelloRetrieveError")] public static partial void TlsClientHelloRetrieveError(ILogger logger, ulong requestId, uint win32Error); + + [LoggerMessage(LoggerEventIds.QueryTlsCipherSuiteError, LogLevel.Debug, "Failed to invoke QueryTlsCipherSuite; RequestId: {RequestId}; Win32 Error code: {Win32Error}", EventName = "QueryTlsCipherSuiteError")] + public static partial void QueryTlsCipherSuiteError(ILogger logger, ulong requestId, uint win32Error); } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index eba7d33ff3b8..5aefb9df1cf3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Security; using System.Runtime.InteropServices; using System.Security.Principal; using Microsoft.AspNetCore.Http; @@ -219,6 +220,45 @@ internal void ForceCancelRequest() } } + /// + /// Gets TLS cipher suite used for the request, if supported by the OS and http.sys. + /// + /// + /// null, if query of TlsCipherSuite is not supported or the query failed. + /// TlsCipherSuite value, if query is successful. + /// + internal unsafe TlsCipherSuite? GetTlsCipherSuite() + { + if (!HttpApi.SupportsQueryTlsCipherInfo) + { + return default; + } + + var requestId = PinsReleased ? Request.RequestId : RequestId; + + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: IntPtr.Zero, + overlapped: IntPtr.Zero); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + // OS supports querying TlsCipherSuite, but request failed. + Log.QueryTlsCipherSuiteError(Logger, requestId, statusCode); + return null; + } + /// /// Attempts to get the client hello message bytes from the http.sys. /// If successful writes the bytes into , and shows how many bytes were written in . diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp index 8112de09b6e6..b0d5a8b2266b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp @@ -133,6 +133,23 @@ std::wstring Environment::GetDllDirectoryValue() return expandedStr; } +ProcessorArchitecture Environment::GetCurrentProcessArchitecture() +{ + // Use compile-time detection - we know which architectures we support + // and this is the most reliable and efficient approach. IsWow64Process2 + // doesn't show the correct architecture when running under x64 emulation + // on ARM64. +#if defined(_M_ARM64) + return ProcessorArchitecture::ARM64; +#elif defined(_M_AMD64) + return ProcessorArchitecture::AMD64; +#elif defined(_M_IX86) + return ProcessorArchitecture::x86; +#else + static_assert(false, "Unknown target architecture"); +#endif +} + bool Environment::IsRunning64BitProcess() { // Check the bitness of the currently running process diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h index 9e3e1b1bf772..a9e6e85d9ecc 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h @@ -5,6 +5,7 @@ #include #include +#include "ProcessorArchitecture.h" class Environment { @@ -23,6 +24,8 @@ class Environment static bool IsRunning64BitProcess(); static + ProcessorArchitecture GetCurrentProcessArchitecture(); + static HRESULT CopyToDirectory(const std::wstring& source, const std::filesystem::path& destination, bool cleanDest, const std::filesystem::path& directoryToIgnore, int& copiedFileCount); static bool CheckUpToDate(const std::wstring& source, const std::filesystem::path& destination, const std::wstring& extension, const std::filesystem::path& directoryToIgnore); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp index 8233d68a115e..8fc74b47c993 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.cpp @@ -411,7 +411,6 @@ HostFxrResolver::InvokeWhereToFindDotnet() HandleWrapper hThread; CComBSTR pwzDotnetName = nullptr; DWORD dwFilePointer = 0; - BOOL fIsCurrentProcess64Bit = FALSE; DWORD dwExitCode = 0; STRU struDotnetSubstring; STRU struDotnetLocationsString; @@ -426,6 +425,7 @@ HostFxrResolver::InvokeWhereToFindDotnet() securityAttributes.bInheritHandle = TRUE; LOG_INFO(L"Invoking where.exe to find dotnet.exe"); + auto currentProcessArch = Environment::GetCurrentProcessArchitecture(); // Create a read/write pipe that will be used for reading the result of where.exe FINISHED_LAST_ERROR_IF(!CreatePipe(&hStdOutReadPipe, &hStdOutWritePipe, &securityAttributes, 0)); @@ -499,13 +499,9 @@ HostFxrResolver::InvokeWhereToFindDotnet() } FINISHED_IF_FAILED(struDotnetLocationsString.CopyA(pzFileContents, dwNumBytesRead)); - LOG_INFOF(L"where.exe invocation returned: '%ls'", struDotnetLocationsString.QueryStr()); - fIsCurrentProcess64Bit = Environment::IsRunning64BitProcess(); - - LOG_INFOF(L"Current process bitness type detected as isX64=%d", fIsCurrentProcess64Bit); - + // Look for a dotnet.exe that matches the current process architecture while (TRUE) { index = struDotnetLocationsString.IndexOf(L"\r\n", prevIndex); @@ -518,28 +514,38 @@ HostFxrResolver::InvokeWhereToFindDotnet() // \r\n is two wchars, so add 2 here. prevIndex = index + 2; - LOG_INFOF(L"Processing entry '%ls'", struDotnetSubstring.QueryStr()); - - if (fIsCurrentProcess64Bit == IsX64(struDotnetSubstring.QueryStr())) + ProcessorArchitecture dotnetArch = GetFileProcessorArchitecture(struDotnetSubstring.QueryStr()); + if (dotnetArch == currentProcessArch) { - // The bitness of dotnet matched with the current worker process bitness. + LOG_INFOF(L"Found dotnet.exe matching current process architecture (%ls) '%ls'", + ProcessorArchitectureToString(dotnetArch), + struDotnetSubstring.QueryStr()); + return std::make_optional(struDotnetSubstring.QueryStr()); } + else + { + LOG_INFOF(L"Skipping dotnet.exe with non-matching architecture %ls (need %ls). '%ls'", + ProcessorArchitectureToString(dotnetArch), + ProcessorArchitectureToString(currentProcessArch), + struDotnetSubstring.QueryStr()); + } } Finished: return result; } -BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) +// Reads the PE header of the binary to determine its architecture. +ProcessorArchitecture HostFxrResolver::GetFileProcessorArchitecture(const WCHAR* binaryPath) { // Errors while reading from the file shouldn't throw unless // file.exception(bits) is set - std::ifstream file(dotnetPath, std::ios::binary); + std::ifstream file(binaryPath, std::ios::binary); if (!file.is_open()) { - LOG_TRACEF(L"Failed to open file %ls", dotnetPath); - return false; + LOG_TRACEF(L"Failed to open file %ls", binaryPath); + return ProcessorArchitecture::Unknown; } // Read the DOS header @@ -547,8 +553,8 @@ BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) file.read(reinterpret_cast(&dosHeader), sizeof(dosHeader)); if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) // 'MZ' { - LOG_TRACEF(L"%ls is not a valid executable file (missing MZ header).", dotnetPath); - return false; + LOG_TRACEF(L"%ls is not a valid executable file (missing MZ header).", binaryPath); + return ProcessorArchitecture::Unknown; } // Seek to the PE header @@ -559,32 +565,30 @@ BOOL HostFxrResolver::IsX64(const WCHAR* dotnetPath) file.read(reinterpret_cast(&peSignature), sizeof(peSignature)); if (peSignature != IMAGE_NT_SIGNATURE) // 'PE\0\0' { - LOG_TRACEF(L"%ls is not a valid PE file (missing PE header).", dotnetPath); - return false; + LOG_TRACEF(L"%ls is not a valid PE file (missing PE header).", binaryPath); + return ProcessorArchitecture::Unknown; } // Read the file header IMAGE_FILE_HEADER fileHeader{}; file.read(reinterpret_cast(&fileHeader), sizeof(fileHeader)); - // Read the optional header magic field - WORD magic{}; - file.read(reinterpret_cast(&magic), sizeof(magic)); - - // Determine the architecture based on the magic value - if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) + // Determine the architecture based on the machine type + switch (fileHeader.Machine) { - LOG_INFOF(L"%ls is 32-bit", dotnetPath); - return false; - } - else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) - { - LOG_INFOF(L"%ls is 64-bit", dotnetPath); - return true; + case IMAGE_FILE_MACHINE_I386: + LOG_INFOF(L"%ls is x86 (32-bit)", binaryPath); + return ProcessorArchitecture::x86; + case IMAGE_FILE_MACHINE_AMD64: + LOG_INFOF(L"%ls is AMD64 (x64)", binaryPath); + return ProcessorArchitecture::AMD64; + case IMAGE_FILE_MACHINE_ARM64: + LOG_INFOF(L"%ls is ARM64", binaryPath); + return ProcessorArchitecture::ARM64; + default: + LOG_INFOF(L"%ls has unknown architecture (machine type: 0x%X)", binaryPath, fileHeader.Machine); + return ProcessorArchitecture::Unknown; } - - LOG_INFOF(L"%ls is unknown architecture %i", dotnetPath, fileHeader.Machine); - return false; } std::optional diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h index 08ec650aec54..9065e2aecd2b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxrResolver.h @@ -8,8 +8,8 @@ #include #include #include - #include "ErrorContext.h" +#include "ProcessorArchitecture.h" #define READ_BUFFER_SIZE 4096 @@ -74,7 +74,7 @@ class HostFxrResolver const std::filesystem::path & requestedPath ); - static BOOL IsX64(const WCHAR* dotnetPath); + static ProcessorArchitecture GetFileProcessorArchitecture(const WCHAR* binaryPath); struct LocalFreeDeleter { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h new file mode 100644 index 000000000000..195feddcae7b --- /dev/null +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ProcessorArchitecture.h @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once + +enum class ProcessorArchitecture +{ + Unknown, + x86, + AMD64, + ARM64 +}; + +inline const wchar_t* ProcessorArchitectureToString(ProcessorArchitecture arch) +{ + switch (arch) + { + case ProcessorArchitecture::x86: + return L"x86"; + case ProcessorArchitecture::AMD64: + return L"AMD64"; + case ProcessorArchitecture::ARM64: + return L"ARM64"; + case ProcessorArchitecture::Unknown: + default: + return L"Unknown"; + } +} \ No newline at end of file diff --git a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs index 0ff3b86369c6..e3559fa5b1e0 100644 --- a/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs +++ b/src/Servers/IIS/IIS/samples/NativeIISSample/Startup.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -52,6 +53,19 @@ public void Configure(IApplicationBuilder app) await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + Environment.NewLine); await context.Response.WriteAsync(Environment.NewLine); + var handshakeFeature = context.Features.Get(); + if (handshakeFeature is not null) + { + await context.Response.WriteAsync(Environment.NewLine); + await context.Response.WriteAsync("TLS Information:" + Environment.NewLine); + await context.Response.WriteAsync($"Protocol: {handshakeFeature.Protocol}" + Environment.NewLine); + + if (handshakeFeature.NegotiatedCipherSuite.HasValue) + { + await context.Response.WriteAsync($"Cipher Suite: {handshakeFeature.NegotiatedCipherSuite.Value}" + Environment.NewLine); + } + } + await context.Response.WriteAsync("User: " + context.User.Identity.Name + Environment.NewLine); if (_authSchemeProvider != null) { diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 3ddc9315cf66..4a1417ed1b52 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -402,6 +402,8 @@ private void GetTlsHandshakeResults() { var handshake = GetTlsHandshake(); Protocol = (SslProtocols)handshake.Protocol; + + NegotiatedCipherSuite = GetTlsCipherSuite(); #pragma warning disable SYSLIB0058 // Type or member is obsolete CipherAlgorithm = (CipherAlgorithmType)handshake.CipherType; CipherStrength = (int)handshake.CipherStrength; @@ -415,6 +417,28 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } + private unsafe TlsCipherSuite? GetTlsCipherSuite() + { + SecPkgContext_CipherInfo cipherInfo = default; + + var statusCode = NativeMethods.HttpQueryRequestProperty( + RequestId, + (HTTP_REQUEST_PROPERTY)14 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsCipherInfo */, + qualifier: null, + qualifierSize: 0, + output: &cipherInfo, + outputSize: (uint)sizeof(SecPkgContext_CipherInfo), + bytesReturned: null, + overlapped: IntPtr.Zero); + + if (statusCode == NativeMethods.HR_OK) + { + return checked((TlsCipherSuite)cipherInfo.dwCipherSuite); + } + + return default; + } + private unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() { var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; diff --git a/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs new file mode 100644 index 000000000000..1bba1c1afcef --- /dev/null +++ b/src/Shared/HttpSys/NativeInterop/SecPkgContext_CipherInfo.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.HttpSys.Internal; + +// From Schannel.h +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +internal unsafe struct SecPkgContext_CipherInfo +{ + private const int SZ_ALG_MAX_SIZE = 64; + + private readonly int dwVersion; + private readonly int dwProtocol; + public readonly int dwCipherSuite; + private readonly int dwBaseCipherSuite; + private fixed char szCipherSuite[SZ_ALG_MAX_SIZE]; + private fixed char szCipher[SZ_ALG_MAX_SIZE]; + private readonly int dwCipherLen; + private readonly int dwCipherBlockLen; // in bytes + private fixed char szHash[SZ_ALG_MAX_SIZE]; + private readonly int dwHashLen; + private fixed char szExchange[SZ_ALG_MAX_SIZE]; + private readonly int dwMinExchangeLen; + private readonly int dwMaxExchangeLen; + private fixed char szCertificate[SZ_ALG_MAX_SIZE]; + private readonly int dwKeyType; +}