diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs index 4d6ccf6fb0..225ae6c6ef 100644 --- a/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs +++ b/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs @@ -186,6 +186,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal { context.Response.StatusCode = 500; context.Response.Headers[HeaderNames.CacheControl] = "no-cache"; + context.Response.ContentType = "text/html; charset=utf-8"; return errorPage.ExecuteAsync(context); }; } diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index 8405e5e0d3..c1aac9be61 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Hosting/Hosting/src/Resources.resx b/src/Hosting/Hosting/src/Resources.resx index 64d6fe523d..22d1975ba0 100644 --- a/src/Hosting/Hosting/src/Resources.resx +++ b/src/Hosting/Hosting/src/Resources.resx @@ -117,15 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Internal Server Error - - - An error occurred while starting the application. - - - Unknown location - WebHostBuilder allows creation only of a single instance of WebHost diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h index a3a1f52398..3eab02bc7c 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h @@ -28,6 +28,7 @@ #define CS_ASPNETCORE_HANDLER_SET_CURRENT_DIRECTORY L"setCurrentDirectory" #define CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE L"disableStartUpErrorPage" #define CS_ENABLED L"enabled" +#define CS_ASPNETCORE_HANDLER_CALL_STARTUP_HOOK L"callStartupHook" class ConfigurationSection: NonCopyable { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h index eb493ce722..3296c68c11 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/ServerErrorHandler.h @@ -9,31 +9,34 @@ class ServerErrorHandler : public REQUEST_HANDLER { public: + ServerErrorHandler(IHttpContext& pContext, USHORT statusCode, USHORT subStatusCode, const std::string& statusText, HRESULT hr, HINSTANCE module, bool disableStartupPage, int page) noexcept + : ServerErrorHandler(pContext, statusCode, subStatusCode, statusText, hr, module, disableStartupPage, page, std::vector()) + { + } - ServerErrorHandler(IHttpContext &pContext, USHORT statusCode, USHORT subStatusCode, std::string statusText, HRESULT hr, HINSTANCE moduleInstance, bool disableStartupPage, int page) noexcept + ServerErrorHandler(IHttpContext& pContext, USHORT statusCode, USHORT subStatusCode, const std::string& statusText, HRESULT hr, HINSTANCE module, bool disableStartupPage, int page, const std::vector& content) noexcept : REQUEST_HANDLER(pContext), - m_pContext(pContext), - m_HR(hr), - m_disableStartupPage(disableStartupPage), - m_page(page), - m_moduleInstance(moduleInstance), - m_statusCode(statusCode), - m_subStatusCode(subStatusCode), - m_statusText(std::move(statusText)) + m_pContext(pContext), + m_HR(hr), + m_disableStartupPage(disableStartupPage), + m_statusCode(statusCode), + m_subStatusCode(subStatusCode), + m_statusText(std::move(statusText)), + m_page(page), + m_ExceptionInfoContent(content), + m_moduleInstance(module) { } REQUEST_NOTIFICATION_STATUS ExecuteRequestHandler() override { - static std::string s_html500Page = GetHtml(m_moduleInstance, m_page); - - WriteStaticResponse(m_pContext, s_html500Page, m_HR, m_disableStartupPage); + WriteStaticResponse(m_pContext, m_HR, m_disableStartupPage); return RQ_NOTIFICATION_FINISH_REQUEST; } private: - void WriteStaticResponse(IHttpContext& pContext, std::string &page, HRESULT hr, bool disableStartupErrorPage) const + void WriteStaticResponse(IHttpContext& pContext, HRESULT hr, bool disableStartupErrorPage) { if (disableStartupErrorPage) { @@ -49,14 +52,23 @@ private: (USHORT)strlen("text/html"), FALSE ); - dataChunk.DataChunkType = HttpDataChunkFromMemory; - dataChunk.FromMemory.pBuffer = page.data(); - dataChunk.FromMemory.BufferLength = static_cast(page.size()); + dataChunk.DataChunkType = HttpDataChunkFromMemory; + if (m_ExceptionInfoContent.size() > 0) + { + dataChunk.FromMemory.pBuffer = &m_ExceptionInfoContent[0]; + dataChunk.FromMemory.BufferLength = static_cast(m_ExceptionInfoContent.size()); + } + else + { + static std::string s_html500Page = GetHtml(m_moduleInstance, m_page); + dataChunk.FromMemory.pBuffer = s_html500Page.data(); + dataChunk.FromMemory.BufferLength = static_cast(s_html500Page.size()); + } + pResponse->WriteEntityChunkByReference(&dataChunk); } - static std::string GetHtml(HMODULE module, int page) { @@ -91,7 +103,7 @@ private: } } - IHttpContext &m_pContext; + IHttpContext& m_pContext; HRESULT m_HR; bool m_disableStartupPage; int m_page; @@ -99,4 +111,5 @@ private: USHORT m_statusCode; USHORT m_subStatusCode; std::string m_statusText; + std::vector m_ExceptionInfoContent; }; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/inprocess_application_tests.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/inprocess_application_tests.cpp index d2ec985723..567192af7f 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/inprocess_application_tests.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLibTests/inprocess_application_tests.cpp @@ -13,6 +13,7 @@ using ::testing::NiceMock; // Externals defined in inprocess BOOL g_fProcessDetach; HANDLE g_hEventLog; +std::wstring g_exceptionEventLog; namespace InprocessTests { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc index 25a99683c5..969377c22b 100644 Binary files a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc and b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/HtmlResponses.rc differ diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp index 568939e84a..3c77516e90 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp @@ -57,6 +57,7 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc const auto handlerSettings = aspNetCoreSection->GetKeyValuePairs(CS_ASPNETCORE_HANDLER_SETTINGS); m_fSetCurrentDirectory = equals_ignore_case(find_element(handlerSettings, CS_ASPNETCORE_HANDLER_SET_CURRENT_DIRECTORY).value_or(L"true"), L"true"); + m_fCallStartupHook = equals_ignore_case(find_element(handlerSettings, CS_ASPNETCORE_HANDLER_CALL_STARTUP_HOOK).value_or(L"true"), L"true"); m_dwStartupTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT) * 1000; m_dwShutdownTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT) * 1000; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h index e1df82aedd..d6eec81d19 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h @@ -48,6 +48,12 @@ public: return m_fSetCurrentDirectory; } + bool + QueryCallStartupHook() const + { + return m_fCallStartupHook; + } + bool QueryWindowsAuthEnabled() const { @@ -116,6 +122,7 @@ private: bool m_fStdoutLogEnabled; bool m_fDisableStartUpErrorPage; bool m_fSetCurrentDirectory; + bool m_fCallStartupHook; bool m_fWindowsAuthEnabled; bool m_fBasicAuthEnabled; bool m_fAnonymousAuthEnabled; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h index 72b26d857c..6db5fee164 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h @@ -15,30 +15,28 @@ public: IHttpApplication& pApplication, HINSTANCE moduleInstance, BOOL disableLogs, - HRESULT hr) + HRESULT hr, + std::vector&& errorPageContent + ) : m_disableLogs(disableLogs), m_HR(hr), m_moduleInstance(moduleInstance), + m_errorPageContent(std::move(errorPageContent)), InProcessApplicationBase(pServer, pApplication) { } ~StartupExceptionApplication() = default; - HRESULT CreateHandler(IHttpContext *pHttpContext, IREQUEST_HANDLER ** pRequestHandler) + HRESULT CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) { - *pRequestHandler = new ServerErrorHandler(*pHttpContext, 500, 30, "Internal Server Error", m_HR, m_moduleInstance, m_disableLogs, IN_PROCESS_RH_STATIC_HTML); + *pRequestHandler = new ServerErrorHandler(*pHttpContext, 500, 30, "Internal Server Error", m_HR, m_moduleInstance, m_disableLogs, IN_PROCESS_RH_STATIC_HTML, m_errorPageContent); + return S_OK; } - std::string& - GetStaticHtml500Content() - { - return html500Page; - } - private: - std::string html500Page; + std::vector m_errorPageContent; BOOL m_disableLogs; HRESULT m_HR; HINSTANCE m_moduleInstance; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp index 948a08950d..710bbee754 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp @@ -28,6 +28,7 @@ HINSTANCE g_hWinHttpModule; HINSTANCE g_hAspNetCoreModule; HANDLE g_hEventLog = NULL; bool g_fInProcessApplicationCreated = false; +std::vector g_errorPageContent; HINSTANCE g_hServerModule; HRESULT @@ -128,7 +129,7 @@ CreateApplication( std::unique_ptr options; THROW_IF_FAILED(InProcessOptions::Create(*pServer, pSite, *pHttpApplication, options)); // Set the currently running application to a fake application that returns startup exceptions. - auto pErrorApplication = std::make_unique(*pServer, *pHttpApplication, g_hServerModule, options->QueryDisableStartUpErrorPage(), hr); + auto pErrorApplication = std::make_unique(*pServer, *pHttpApplication, g_hServerModule, options->QueryDisableStartUpErrorPage(), hr, std::move(g_errorPageContent)); RETURN_IF_FAILED(pErrorApplication->StartMonitoringAppOffline()); *ppApplication = pErrorApplication.release(); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 696211420a..d8991843d1 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -235,8 +235,26 @@ IN_PROCESS_APPLICATION::ExecuteApplication() LOG_INFOF(L"Setting current directory to %s", this->QueryApplicationPhysicalPath().c_str()); } - bool clrThreadExited; + if (m_pConfig->QueryCallStartupHook()) + { + // Used to display developer exception page when there is an exception in main. + auto currentStartupHookEnv = Environment::GetEnvironmentVariableValue(DOTNETCORE_STARTUP_HOOK); + if (currentStartupHookEnv.has_value()) + { + currentStartupHookEnv = currentStartupHookEnv.value() + L";" + ASPNETCORE_STARTUP_ASSEMBLY; + LOG_LAST_ERROR_IF(!SetEnvironmentVariable(DOTNETCORE_STARTUP_HOOK, currentStartupHookEnv.value().c_str())); + } + else + { + LOG_LAST_ERROR_IF(!SetEnvironmentVariable(DOTNETCORE_STARTUP_HOOK, ASPNETCORE_STARTUP_ASSEMBLY)); + } + } + + // Used to make .NET Runtime always log to event log when there is an unhandled exception. + LOG_LAST_ERROR_IF(SetEnvironmentVariable(L"COMPlus_UseEntryPointFilter", L"1")); + + bool clrThreadExited; { auto redirectionOutput = LoggingHelpers::CreateOutputs( m_pConfig->QueryStdoutLogEnabled(), diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index 349b2e2932..9f28db3889 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -15,6 +15,8 @@ typedef BOOL(WINAPI * PFN_SHUTDOWN_HANDLER) (void* pvShutdownHandlerContext); typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_ASYNC_COMPLETION_HANDLER)(void *pvManagedHttpContext, HRESULT hrCompletionStatus, DWORD cbCompletion); typedef void(WINAPI * PFN_REQUESTS_DRAINED_HANDLER) (void* pvShutdownHandlerContext); +#define DOTNETCORE_STARTUP_HOOK L"DOTNET_STARTUP_HOOKS" +#define ASPNETCORE_STARTUP_ASSEMBLY L"Microsoft.AspNetCore.Server.IIS" class IN_PROCESS_APPLICATION : public InProcessApplicationBase { public: @@ -58,7 +60,6 @@ public: HRESULT LoadManagedApplication(); - void QueueStop(); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index 93c9d6a97b..48bddcc827 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -4,8 +4,11 @@ #include "inprocessapplication.h" #include "inprocesshandler.h" #include "requesthandler_config.h" +#include "EventLog.h" extern bool g_fInProcessApplicationCreated; +extern std::vector g_errorPageContent; +extern IHttpServer* g_pHttpServer; // // Initialization export @@ -236,7 +239,7 @@ http_read_request_bytes( { return E_FAIL; } - IHttpRequest *pHttpRequest = (IHttpRequest*)pInProcessHandler->QueryHttpContext()->GetRequest(); + IHttpRequest* pHttpRequest = (IHttpRequest*)pInProcessHandler->QueryHttpContext()->GetRequest(); // Check if there is anything to read if (pHttpRequest->GetRemainingEntityBytes() > 0) @@ -267,7 +270,7 @@ http_write_response_bytes( _In_ BOOL* pfCompletionExpected ) { - IHttpResponse *pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); + IHttpResponse* pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); BOOL fAsync = TRUE; BOOL fMoreData = TRUE; DWORD dwBytesSent = 0; @@ -291,7 +294,7 @@ http_flush_response_bytes( _Out_ BOOL* pfCompletionExpected ) { - IHttpResponse *pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); + IHttpResponse* pHttpResponse = (IHttpResponse*)pInProcessHandler->QueryHttpContext()->GetResponse(); BOOL fAsync = TRUE; DWORD dwBytesSent = 0; @@ -316,7 +319,7 @@ http_websockets_read_bytes( _In_ BOOL* pfCompletionPending ) { - IHttpRequest3 *pHttpRequest = (IHttpRequest3*)pInProcessHandler->QueryHttpContext()->GetRequest(); + IHttpRequest3* pHttpRequest = (IHttpRequest3*)pInProcessHandler->QueryHttpContext()->GetRequest(); BOOL fAsync = TRUE; @@ -343,7 +346,7 @@ http_websockets_write_bytes( _In_ BOOL* pfCompletionExpected ) { - IHttpResponse2 *pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); + IHttpResponse2* pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); BOOL fAsync = TRUE; BOOL fMoreData = TRUE; @@ -371,7 +374,7 @@ http_websockets_flush_bytes( _In_ BOOL* pfCompletionExpected ) { - IHttpResponse2 *pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); + IHttpResponse2* pHttpResponse = (IHttpResponse2*)pInProcessHandler->QueryHttpContext()->GetResponse(); BOOL fAsync = TRUE; BOOL fMoreData = TRUE; @@ -504,4 +507,12 @@ set_main_handler(_In_ hostfxr_main_fn main) IN_PROCESS_APPLICATION::SetMainCallback(main); } +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +VOID +http_set_startup_error_page_content(_In_ byte* errorPageContent, int length) +{ + g_errorPageContent.resize(length); + memcpy(&g_errorPageContent[0], errorPageContent, length); +} + // End of export diff --git a/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.csproj b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.csproj index 6b6ca050cb..ed6753b780 100644 --- a/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.csproj +++ b/src/Servers/IIS/IIS/ref/Microsoft.AspNetCore.Server.IIS.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj index 293ab4c807..dd160c168a 100644 --- a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj +++ b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -14,8 +14,11 @@ - - + + + + + @@ -32,6 +35,8 @@ + + diff --git a/src/Servers/IIS/IIS/src/NativeMethods.cs b/src/Servers/IIS/IIS/src/NativeMethods.cs index 415604ccc1..3fa84190d8 100644 --- a/src/Servers/IIS/IIS/src/NativeMethods.cs +++ b/src/Servers/IIS/IIS/src/NativeMethods.cs @@ -147,6 +147,9 @@ namespace Microsoft.AspNetCore.Server.IIS [DllImport(AspNetCoreModuleDll)] private static extern int http_get_authentication_information(IntPtr pInProcessHandler, [MarshalAs(UnmanagedType.BStr)] out string authType, out IntPtr token); + [DllImport(AspNetCoreModuleDll)] + private static extern unsafe int http_set_startup_error_page_content(byte* content, int contentLength); + public static void HttpPostCompletion(IntPtr pInProcessHandler, int cbBytes) { Validate(http_post_completion(pInProcessHandler, cbBytes)); @@ -169,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.IIS Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, disconnectCallback, asyncCallback, requestsDrainedHandler, pvRequestContext, pvShutdownContext)); } - public static unsafe int HttpWriteResponseBytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected) + internal static unsafe int HttpWriteResponseBytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected) { return http_write_response_bytes(pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected); } @@ -179,7 +182,7 @@ namespace Microsoft.AspNetCore.Server.IIS return http_flush_response_bytes(pInProcessHandler, fMoreData, out fCompletionExpected); } - public static unsafe HttpApiTypes.HTTP_REQUEST_V2* HttpGetRawRequest(IntPtr pInProcessHandler) + internal static unsafe HttpApiTypes.HTTP_REQUEST_V2* HttpGetRawRequest(IntPtr pInProcessHandler) { return http_get_raw_request(pInProcessHandler); } @@ -219,7 +222,7 @@ namespace Microsoft.AspNetCore.Server.IIS Validate(http_set_managed_context(pInProcessHandler, pvManagedContext)); } - public static IISConfigurationData HttpGetApplicationProperties() + internal static IISConfigurationData HttpGetApplicationProperties() { var iisConfigurationData = new IISConfigurationData(); Validate(http_get_application_properties(ref iisConfigurationData)); @@ -247,7 +250,7 @@ namespace Microsoft.AspNetCore.Server.IIS return http_websockets_read_bytes(pInProcessHandler, pvBuffer, cbBuffer, pfnCompletionCallback, pvCompletionContext, out dwBytesReceived, out fCompletionExpected); } - public static unsafe int HttpWebsocketsWriteBytes( + internal static unsafe int HttpWebsocketsWriteBytes( IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, @@ -297,6 +300,14 @@ namespace Microsoft.AspNetCore.Server.IIS Validate(http_get_authentication_information(pInProcessHandler, out authType, out token)); } + internal static unsafe void HttpSetStartupErrorPageContent(byte[] content) + { + fixed(byte* bytePtr = content) + { + http_set_startup_error_page_content(bytePtr, content.Length); + } + } + private static void Validate(int hr) { if (hr != HR_OK) diff --git a/src/Servers/IIS/IIS/src/StartupHook.cs b/src/Servers/IIS/IIS/src/StartupHook.cs new file mode 100644 index 0000000000..436a1cd079 --- /dev/null +++ b/src/Servers/IIS/IIS/src/StartupHook.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Views; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.IIS; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.StackTrace.Sources; + + +/// +/// Startup hooks are pieces of code that will run before a users program main executes +/// See: https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/host-startup-hook.md +/// The type must be named StartupHook without any namespace, and should be internal. +/// +internal class StartupHook +{ + /// + /// Startup hooks are pieces of code that will run before a users program main executes + /// See: https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/host-startup-hook.md + /// + public static void Initialize() + { + if (!NativeMethods.IsAspNetCoreModuleLoaded()) + { + // This means someone set the startup hook for Microsoft.AspNetCore.Server.IIS + // but are not running inprocess. Return at this point. + return; + } + + var detailedErrors = Environment.GetEnvironmentVariable("ASPNETCORE_DETAILEDERRORS"); + var enableStartupErrorPage = detailedErrors?.Equals("1", StringComparison.OrdinalIgnoreCase) ?? false; + enableStartupErrorPage |= detailedErrors?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; + + var aspnetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + enableStartupErrorPage |= aspnetCoreEnvironment?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false; + + var dotnetEnvironment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); + enableStartupErrorPage |= dotnetEnvironment?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false; + + if (!enableStartupErrorPage) + { + // Not running in development or detailed errors aren't enabled + return; + } + + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => + { + var exception = (Exception)eventArgs.ExceptionObject; + + // Get the content root from IIS. + var iisConfigData = NativeMethods.HttpGetApplicationProperties(); + var contentRoot = iisConfigData.pwzFullApplicationPath.TrimEnd(Path.DirectorySeparatorChar); + + var model = new ErrorPageModel + { + RuntimeDisplayName = RuntimeInformation.FrameworkDescription + }; + + var systemRuntimeAssembly = typeof(System.ComponentModel.DefaultValueAttribute).GetTypeInfo().Assembly; + var assemblyVersion = new AssemblyName(systemRuntimeAssembly.FullName).Version.ToString(); + var clrVersion = assemblyVersion; + model.RuntimeArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + var currentAssembly = typeof(ErrorPage).GetTypeInfo().Assembly; + model.CurrentAssemblyVesion = currentAssembly + .GetCustomAttribute() + .InformationalVersion; + model.ClrVersion = clrVersion; + model.OperatingSystemDescription = RuntimeInformation.OSDescription; + + var exceptionDetailProvider = new ExceptionDetailsProvider( + new PhysicalFileProvider(contentRoot), + sourceCodeLineCount: 6); + + model.ErrorDetails = exceptionDetailProvider.GetDetails(exception); + + var errorPage = new ErrorPage(model); + + var stream = new MemoryStream(); + + // Never will go async because we are writing to a memory stream. + errorPage.ExecuteAsync(stream).GetAwaiter().GetResult(); + + // Get the raw content and set the error page. + stream.Position = 0; + var content = stream.ToArray(); + + NativeMethods.HttpSetStartupErrorPageContent(content); + }; + } +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index 4ded2cb99b..a3ae391fbe 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -481,6 +483,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests private static Dictionary> StandaloneConfigTransformations = InitStandaloneConfigTransformations(); + public static IEnumerable StandaloneConfigTransformationsScenarios => StandaloneConfigTransformations.ToTheoryData(); [ConditionalTheory] @@ -572,6 +575,116 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests await request; } + [ConditionalTheory] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewHandler] + [InlineData("ASPNETCORE_ENVIRONMENT", "Development")] + [InlineData("DOTNET_ENVIRONMENT", "deVelopment")] + [InlineData("ASPNETCORE_DETAILEDERRORS", "1")] + [InlineData("ASPNETCORE_DETAILEDERRORS", "TRUE")] + public async Task ExceptionIsLoggedToEventLogAndPutInResponseWhenDeveloperExceptionPageIsEnabled(string environmentVariable, string value) + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} Throw"); + + // Deployment parameters by default set ASPNETCORE_DETAILEDERRORS to true + deploymentParameters.EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = ""; + deploymentParameters.EnvironmentVariables[environmentVariable] = value; + + var deploymentResult = await DeployAsync(deploymentParameters); + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.False(result.IsSuccessStatusCode); + + var content = await result.Content.ReadAsStringAsync(); + Assert.Contains("InvalidOperationException", content); + Assert.Contains("TestSite.Program.Main", content); + + StopServer(); + + VerifyDotnetRuntimeEventLog(deploymentResult); + } + + [ConditionalFact] + [RequiresIIS(IISCapability.PoolEnvironmentVariables)] + [RequiresNewHandler] + public async Task ExceptionIsNotLoggedToResponseWhenStartupHookIsDisabled() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} Throw"); + deploymentParameters.EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = ""; + deploymentParameters.EnvironmentVariables["ASPNETCORE_ENVIRONMENT"] = "Development"; + deploymentParameters.HandlerSettings["callStartupHook"] = "false"; + + var deploymentResult = await DeployAsync(deploymentParameters); + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.False(result.IsSuccessStatusCode); + + var content = await result.Content.ReadAsStringAsync(); + Assert.DoesNotContain("InvalidOperationException", content); + + StopServer(); + + VerifyDotnetRuntimeEventLog(deploymentResult); + } + + [ConditionalFact] + [RequiresNewHandler] + public async Task ExceptionIsLoggedToEventLogDoesNotWriteToResponse() + { + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.TransformArguments((a, _) => $"{a} Throw"); + + // Deployment parameters by default set ASPNETCORE_DETAILEDERRORS to true + deploymentParameters.EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = ""; + + var deploymentResult = await DeployAsync(deploymentParameters); + var result = await deploymentResult.HttpClient.GetAsync("/"); + Assert.False(result.IsSuccessStatusCode); + + var content = await result.Content.ReadAsStringAsync(); + Assert.DoesNotContain("InvalidOperationException", content); + + StopServer(); + + VerifyDotnetRuntimeEventLog(deploymentResult); + } + + private static void VerifyDotnetRuntimeEventLog(IISDeploymentResult deploymentResult) + { + var entries = GetEventLogsFromDotnetRuntime(deploymentResult); + + var expectedRegex = new Regex("Exception Info: System\\.InvalidOperationException:", RegexOptions.Singleline); + var matchedEntries = entries.Where(entry => expectedRegex.IsMatch(entry.Message)).ToArray(); + // There isn't a process ID to filter on here, so there can be duplicate entries from other tests. + Assert.True(matchedEntries.Length > 0); + } + + private static IEnumerable GetEventLogsFromDotnetRuntime(IISDeploymentResult deploymentResult) + { + var processStartTime = deploymentResult.HostProcess.StartTime.AddSeconds(-5); + var eventLog = new EventLog("Application"); + + for (var i = eventLog.Entries.Count - 1; i >= 0; i--) + { + var eventLogEntry = eventLog.Entries[i]; + if (eventLogEntry.TimeGenerated < processStartTime) + { + // If event logs is older than the process start time, we didn't find a match. + break; + } + + if (eventLogEntry.ReplacementStrings == null) + { + continue; + } + + if (eventLogEntry.Source == ".NET Runtime") + { + yield return eventLogEntry; + } + } + } + private static void MoveApplication( IISDeploymentParameters parameters, string subdirectory) diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/MultiApplicationTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/MultiApplicationTests.cs index 90bbcb4291..87d9c73dd0 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/MultiApplicationTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/MultiApplicationTests.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Extensions.Logging; using Xunit; namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests diff --git a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs b/src/Shared/ErrorPage/ErrorPage.Designer.cs similarity index 92% rename from src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs rename to src/Shared/ErrorPage/ErrorPage.Designer.cs index 52a6db45d3..4cfcd48f6b 100644 --- a/src/Hosting/Hosting/src/Startup/ExceptionPage/Views/ErrorPage.Designer.cs +++ b/src/Shared/ErrorPage/ErrorPage.Designer.cs @@ -1,1055 +1,1055 @@ -namespace Microsoft.AspNetCore.Hosting.Views -{ -#line 1 "ErrorPage.cshtml" -using System - -#line default -#line hidden - ; -#line 2 "ErrorPage.cshtml" -using System.Globalization - -#line default -#line hidden - ; -#line 3 "ErrorPage.cshtml" -using System.Linq - -#line default -#line hidden - ; -#line 4 "ErrorPage.cshtml" -using System.Net - -#line default -#line hidden - ; -#line 5 "ErrorPage.cshtml" -using System.Reflection - -#line default -#line hidden - ; -#line 6 "ErrorPage.cshtml" -using Microsoft.AspNetCore.Hosting.Views - -#line default -#line hidden - ; - using System.Threading.Tasks; - - internal class ErrorPage : Microsoft.Extensions.RazorViews.BaseView - { -#line 9 "ErrorPage.cshtml" - - public ErrorPage(ErrorPageModel model) - { - Model = model; - } - - public ErrorPageModel Model { get; set; } - -#line default -#line hidden - #line hidden - public ErrorPage() - { - } - - #pragma warning disable 1998 - public override async Task ExecuteAsync() - { - WriteLiteral("\r\n"); -#line 17 "ErrorPage.cshtml" - - Response.ContentType = "text/html; charset=utf-8"; - var location = string.Empty; - -#line default -#line hidden - - WriteLiteral("\r\n