From b6c311c14d3c3cf89b1b28d52efa0088a580213e Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Tue, 18 Sep 2018 16:36:52 -0700 Subject: [PATCH] Add client disconnect and connection abort support (#1388) --- build/testsite.props | 4 +- .../AspNetCore/AspNetCore.vcxproj | 2 + .../AspNetCore/DisconnectHandler.cpp | 33 +++ .../AspNetCore/DisconnectHandler.h | 35 +++ src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp | 10 +- .../AspNetCore/proxymodule.cpp | 50 ++++- .../AspNetCore/proxymodule.h | 15 +- .../CommonLib/irequesthandler.h | 4 +- .../CommonLib/requesthandler.h | 8 +- .../inprocessapplication.cpp | 30 +-- .../inprocessapplication.h | 17 +- .../inprocesshandler.cpp | 22 +- .../inprocesshandler.h | 9 +- .../managedexports.cpp | 12 + .../OutOfProcessRequestHandler.vcxproj | 1 - .../disconnectcontext.h | 78 ------- .../forwardinghandler.cpp | 51 +---- .../forwardinghandler.h | 7 +- .../OutOfProcessRequestHandler/stdafx.h | 1 - .../Core/DuplexStream.cs | 1 - .../Core/EmptyStream.cs | 94 ++++++++ .../Core/HttpRequestStream.cs | 160 ++++++++++++++ .../Core/HttpResponseStream.cs | 151 +++++++++++++ .../Core/HttpStreamState.cs | 12 + .../Core/HttpUpgradeStream.cs | 208 ++++++++++++++++++ .../Core/IISHttpContext.FeatureCollection.cs | 24 +- ...HttpContext.IHttpRequestLifetimeFeature.cs | 71 ++++++ ...text.ReadWrite.cs => IISHttpContext.IO.cs} | 57 ++++- .../Core/IISHttpContext.cs | 37 ++-- .../Core/IISHttpContextOfT.cs | 4 +- .../Core/IISHttpResponseBody.cs | 65 ------ .../Core/IISHttpServer.cs | 19 +- .../Core/IISNativeApplication.cs | 4 +- .../Core/OutputProducer.cs | 17 +- .../Core/ReadOnlyStream.cs | 63 ++++++ .../Core/Streams.cs | 68 ++++++ .../ThrowingWasUpgradedWriteOnlyStream.cs | 28 +++ .../Core/WrappingStream.cs | 144 ++++++++++++ ...SHttpRequestBody.cs => WriteOnlyStream.cs} | 33 +-- .../CoreStrings.resx | 150 +++++++++++++ .../NativeMethods.cs | 13 +- .../Properties/CoreStrings.Designer.cs | 170 ++++++++++++++ .../Common.FunctionalTests/AppOfflineTests.cs | 2 +- .../Utilities/Helpers.cs | 2 +- .../Common.Tests/Utilities/TestConnections.cs | 1 + .../Utilities/TimeoutExtensions.cs | 11 +- test/IIS.Tests/ClientDisconnectTests.cs | 105 +++++++-- test/IIS.Tests/ConnectionIdFeatureTests.cs | 54 +++++ test/IIS.Tests/HttpBodyControlFeatureTests.cs | 31 +++ test/IIS.Tests/ResponseAbortTests.cs | 152 +++++++++++++ test/IIS.Tests/StrictTestServerTests.cs | 24 ++ .../InProcessWebSite/InProcessWebSite.csproj | 6 +- 52 files changed, 2021 insertions(+), 349 deletions(-) create mode 100644 src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp create mode 100644 src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h delete mode 100644 src/AspNetCoreModuleV2/OutOfProcessRequestHandler/disconnectcontext.h create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs rename src/Microsoft.AspNetCore.Server.IIS/Core/{IISHttpContext.ReadWrite.cs => IISHttpContext.IO.cs} (81%) delete mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpResponseBody.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs rename src/Microsoft.AspNetCore.Server.IIS/Core/{IISHttpRequestBody.cs => WriteOnlyStream.cs} (57%) create mode 100644 src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx create mode 100644 src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs create mode 100644 test/IIS.Tests/ConnectionIdFeatureTests.cs create mode 100644 test/IIS.Tests/HttpBodyControlFeatureTests.cs create mode 100644 test/IIS.Tests/ResponseAbortTests.cs create mode 100644 test/IIS.Tests/StrictTestServerTests.cs diff --git a/build/testsite.props b/build/testsite.props index ad7204aab4..861d44a563 100644 --- a/build/testsite.props +++ b/build/testsite.props @@ -36,8 +36,8 @@ /config:"$(IISExpressAppHostConfig)" /systray:false -h "$(IISAppHostConfig)" - $(NativePlatform)\aspnetcore.dll - $(NativePlatform)\aspnetcorev2.dll + $(AspNetCoreModuleV1ShimDll) + $(AspNetCoreModuleV2ShimDll) aspnetcorev2_inprocess.dll $(userprofile)\.dotnet\$(NativePlatform)\dotnet.exe diff --git a/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj b/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj index b71ab41c83..bd0129be53 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj +++ b/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj @@ -231,6 +231,7 @@ + @@ -246,6 +247,7 @@ + diff --git a/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp b/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp new file mode 100644 index 0000000000..b921994850 --- /dev/null +++ b/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.cpp @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#include "DisconnectHandler.h" +#include "exceptions.h" +#include "proxymodule.h" + +void DisconnectHandler::NotifyDisconnect() +{ + try + { + const auto module = m_pModule.exchange(nullptr); + if (module != nullptr) + { + module ->NotifyDisconnect(); + } + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + } +} + +void DisconnectHandler::CleanupStoredContext() noexcept +{ + SetHandler(nullptr); + delete this; +} + +void DisconnectHandler::SetHandler(ASPNET_CORE_PROXY_MODULE * module) noexcept +{ + m_pModule = module; +} diff --git a/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h b/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h new file mode 100644 index 0000000000..34c8a24a63 --- /dev/null +++ b/src/AspNetCoreModuleV2/AspNetCore/DisconnectHandler.h @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#pragma once +#include + +class ASPNET_CORE_PROXY_MODULE; + +class DisconnectHandler final: public IHttpConnectionStoredContext +{ +public: + DisconnectHandler() + : m_pModule(nullptr) + { + } + + virtual + ~DisconnectHandler() + { + SetHandler(nullptr); + } + + void + NotifyDisconnect() override; + + void + CleanupStoredContext() noexcept override; + + void + SetHandler(ASPNET_CORE_PROXY_MODULE * module) noexcept; + +private: + std::atomic m_pModule; +}; + diff --git a/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp b/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp index 4ec0d774dd..3fce85afdd 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/dllmain.cpp @@ -41,7 +41,7 @@ BOOL WINAPI DllMain(HMODULE hModule, switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: - + ALLOC_CACHE_HANDLER::StaticInitialize(); g_hServerModule = hModule; DisableThreadLibraryCalls(hModule); @@ -101,7 +101,7 @@ HRESULT { g_hEventLog = RegisterEventSource(nullptr, ASPNETCORE_EVENT_PROVIDER); } - + // check whether the feature is disabled due to security reason if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\IIS Extensions\\IIS AspNetCore Module V2\\Parameters", @@ -144,9 +144,9 @@ HRESULT // The ASPNET_CORE_PROXY_MODULE_FACTORY::Terminate method will clean any // static object initialized. // - + auto applicationManager = std::make_shared(g_hServerModule, *pHttpServer); - auto moduleFactory = std::make_unique(applicationManager); + auto moduleFactory = std::make_unique(pModuleInfo->GetId(), applicationManager); RETURN_IF_FAILED(pModuleInfo->SetRequestNotifications( moduleFactory.release(), @@ -159,7 +159,7 @@ HRESULT pGlobalModule.release(), GL_CONFIGURATION_CHANGE | // Configuration change trigers IIS application stop GL_STOP_LISTENING)); // worker process stop or recycle - + return S_OK; } CATCH_RETURN() diff --git a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp index 8fafe13c1a..d3d71e8b4b 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp @@ -6,13 +6,15 @@ #include "applicationmanager.h" #include "applicationinfo.h" #include "exceptions.h" +#include "DisconnectHandler.h" extern BOOL g_fInShutdown; __override -ASPNET_CORE_PROXY_MODULE_FACTORY::ASPNET_CORE_PROXY_MODULE_FACTORY(std::shared_ptr applicationManager) noexcept - :m_pApplicationManager(std::move(applicationManager)) +ASPNET_CORE_PROXY_MODULE_FACTORY::ASPNET_CORE_PROXY_MODULE_FACTORY(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept + : m_pApplicationManager(std::move(applicationManager)), + m_moduleId(moduleId) { } @@ -22,10 +24,10 @@ ASPNET_CORE_PROXY_MODULE_FACTORY::GetHttpModule( IModuleAllocator * pAllocator ) { - + #pragma warning( push ) #pragma warning ( disable : 26409 ) // Disable "Avoid using new" - *ppModule = new (pAllocator) ASPNET_CORE_PROXY_MODULE(m_pApplicationManager); + *ppModule = new (pAllocator) ASPNET_CORE_PROXY_MODULE(m_moduleId, m_pApplicationManager); #pragma warning( push ) if (*ppModule == nullptr) { @@ -58,13 +60,23 @@ Return value: delete this; } -ASPNET_CORE_PROXY_MODULE::ASPNET_CORE_PROXY_MODULE(std::shared_ptr applicationManager) noexcept +ASPNET_CORE_PROXY_MODULE::ASPNET_CORE_PROXY_MODULE(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept : m_pApplicationManager(std::move(applicationManager)), m_pApplicationInfo(nullptr), - m_pHandler(nullptr) + m_pHandler(nullptr), + m_moduleId(moduleId), + m_pDisconnectHandler(nullptr) { } +ASPNET_CORE_PROXY_MODULE::~ASPNET_CORE_PROXY_MODULE() +{ + if (m_pDisconnectHandler != nullptr) + { + m_pDisconnectHandler->SetHandler(nullptr); + } +} + __override REQUEST_NOTIFICATION_STATUS ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler( @@ -77,12 +89,31 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler( try { - if (g_fInShutdown) { FINISHED(HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS)); } + auto moduleContainer = pHttpContext + ->GetConnection() + ->GetModuleContextContainer(); + + #pragma warning( push ) + #pragma warning ( disable : 26466 ) // Disable "Don't use static_cast downcasts". We build without RTTI support so dynamic_cast is not available + m_pDisconnectHandler = static_cast(moduleContainer->GetConnectionModuleContext(m_moduleId)); + #pragma warning( push ) + + if (m_pDisconnectHandler == nullptr) + { + auto disconnectHandler = std::make_unique(); + m_pDisconnectHandler = disconnectHandler.get(); + // ModuleContextContainer takes ownership of disconnectHandler + // we are trusting that it would not release it before deleting the context + FINISHED_IF_FAILED(moduleContainer->SetConnectionModuleContext(static_cast(disconnectHandler.release()), m_moduleId)); + } + + m_pDisconnectHandler->SetHandler(this); + FINISHED_IF_FAILED(m_pApplicationManager->GetOrCreateApplicationInfo( *pHttpContext, m_pApplicationInfo)); @@ -135,3 +166,8 @@ ASPNET_CORE_PROXY_MODULE::OnAsyncCompletion( return RQ_NOTIFICATION_FINISH_REQUEST; } } + +void ASPNET_CORE_PROXY_MODULE::NotifyDisconnect() const +{ + m_pHandler->NotifyDisconnect(); +} diff --git a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h index 7c67712bab..97c126b0f9 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h +++ b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.h @@ -7,6 +7,7 @@ #include "applicationinfo.h" #include "irequesthandler.h" #include "applicationmanager.h" +#include "DisconnectHandler.h" extern HTTP_MODULE_ID g_pModuleId; @@ -14,9 +15,9 @@ class ASPNET_CORE_PROXY_MODULE : NonCopyable, public CHttpModule { public: - ASPNET_CORE_PROXY_MODULE(std::shared_ptr applicationManager) noexcept; + ASPNET_CORE_PROXY_MODULE(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept; - ~ASPNET_CORE_PROXY_MODULE() = default; + ~ASPNET_CORE_PROXY_MODULE(); void * operator new(size_t size, IModuleAllocator * pPlacement) { @@ -46,16 +47,21 @@ class ASPNET_CORE_PROXY_MODULE : NonCopyable, public CHttpModule IHttpCompletionInfo * pCompletionInfo ) override; + void + NotifyDisconnect() const; + private: std::shared_ptr m_pApplicationManager; std::shared_ptr m_pApplicationInfo; std::unique_ptr m_pHandler; + HTTP_MODULE_ID m_moduleId; + DisconnectHandler * m_pDisconnectHandler; }; class ASPNET_CORE_PROXY_MODULE_FACTORY : NonCopyable, public IHttpModuleFactory { public: - ASPNET_CORE_PROXY_MODULE_FACTORY(std::shared_ptr applicationManager) noexcept; + ASPNET_CORE_PROXY_MODULE_FACTORY(HTTP_MODULE_ID moduleId, std::shared_ptr applicationManager) noexcept; virtual ~ASPNET_CORE_PROXY_MODULE_FACTORY() = default; HRESULT @@ -66,7 +72,8 @@ class ASPNET_CORE_PROXY_MODULE_FACTORY : NonCopyable, public IHttpModuleFactory VOID Terminate() noexcept override; - + private: std::shared_ptr m_pApplicationManager; + HTTP_MODULE_ID m_moduleId; }; diff --git a/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h b/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h index 5f9b415794..8bd2076072 100644 --- a/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h +++ b/src/AspNetCoreModuleV2/CommonLib/irequesthandler.h @@ -26,9 +26,7 @@ public: virtual VOID - TerminateRequest( - bool fClientInitiated - ) = 0; + NotifyDisconnect() noexcept(false) = 0; virtual ~IREQUEST_HANDLER( diff --git a/src/AspNetCoreModuleV2/CommonLib/requesthandler.h b/src/AspNetCoreModuleV2/CommonLib/requesthandler.h index 7de54e4c77..a5e16bf022 100644 --- a/src/AspNetCoreModuleV2/CommonLib/requesthandler.h +++ b/src/AspNetCoreModuleV2/CommonLib/requesthandler.h @@ -14,7 +14,7 @@ class REQUEST_HANDLER: public virtual IREQUEST_HANDLER public: VOID - ReferenceRequestHandler() noexcept override + ReferenceRequestHandler() noexcept override { InterlockedIncrement(&m_cRefs); } @@ -39,9 +39,11 @@ public: return RQ_NOTIFICATION_FINISH_REQUEST; } - VOID TerminateRequest(bool fClientInitiated) override + #pragma warning( push ) + #pragma warning ( disable : 26440 ) // Disable "Can be marked with noexcept" + VOID NotifyDisconnect() override + #pragma warning( pop ) { - UNREFERENCED_PARAMETER(fClientInitiated); } private: diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 6f37a1a3b9..7fd934ae87 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -44,14 +44,14 @@ IN_PROCESS_APPLICATION::~IN_PROCESS_APPLICATION() VOID IN_PROCESS_APPLICATION::StopInternal(bool fServerInitiated) -{ +{ StopClr(); InProcessApplicationBase::StopInternal(fServerInitiated); } VOID IN_PROCESS_APPLICATION::StopClr() -{ +{ LOG_INFO(L"Stopping CLR"); if (!m_blockManagedCallbacks) @@ -86,6 +86,7 @@ VOID IN_PROCESS_APPLICATION::SetCallbackHandles( _In_ PFN_REQUEST_HANDLER request_handler, _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, + _In_ PFN_DISCONNECT_HANDLER disconnect_callback, _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext @@ -95,6 +96,7 @@ IN_PROCESS_APPLICATION::SetCallbackHandles( m_RequestHandler = request_handler; m_RequestHandlerContext = pvRequstHandlerContext; + m_DisconnectHandler = disconnect_callback; m_ShutdownHandler = shutdown_handler; m_ShutdownHandlerContext = pvShutdownHandlerContext; m_AsyncCompletionHandler = async_completion_handler; @@ -135,13 +137,13 @@ IN_PROCESS_APPLICATION::LoadManagedApplication() }, ::ReferenceApplication(this)); LOG_INFO(L"Waiting for initialization"); - + const HANDLE waitHandles[2] = { m_pInitializeEvent, m_workerThread.native_handle() }; - + // Wait for shutdown request const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, m_pConfig->QueryStartupTimeLimitInMS()); THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); - + if (waitResult == WAIT_TIMEOUT) { // If server wasn't initialized in time shut application down without waiting for CLR thread to exit @@ -168,9 +170,9 @@ void IN_PROCESS_APPLICATION::ExecuteApplication() { try - { + { std::unique_ptr hostFxrOptions; - + auto context = std::make_shared(); auto pProc = s_fMainCallback; @@ -184,7 +186,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() // Get the entry point for main pProc = reinterpret_cast(GetProcAddress(hModule, "hostfxr_main")); THROW_LAST_ERROR_IF_NULL(pProc); - + THROW_IF_FAILED(HOSTFXR_OPTIONS::Create( m_dotnetExeKnownLocation, m_pConfig->QueryProcessPath(), @@ -217,7 +219,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() // We set a static so that managed code can call back into this instance and // set the callbacks s_Application = this; - + //Start CLR thread m_clrThread = std::thread(ClrThreadEntryPoint, context); @@ -237,7 +239,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() { const auto clrWaitResult = WaitForSingleObject(m_clrThread.native_handle(), m_pConfig->QueryShutdownTimeLimitInMS()); THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); - + clrThreadExited = clrWaitResult != WAIT_TIMEOUT; } @@ -274,7 +276,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() // in case when it was not initialized we need to keep server running to serve 502 page if (m_Initialized) { - QueueStop(); + QueueStop(); } } } @@ -298,7 +300,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() QueryApplicationId().c_str(), QueryApplicationPhysicalPath().c_str(), GetUnexpectedExceptionMessage(ex).c_str()); - + OBSERVE_CAUGHT_EXCEPTION(); } } @@ -500,9 +502,9 @@ IN_PROCESS_APPLICATION::CreateHandler( { try { - *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_AsyncCompletionHandler); + *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_DisconnectHandler, m_AsyncCompletionHandler); } CATCH_RETURN(); - + return S_OK; } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index e2488d71e7..caf16193d6 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -10,6 +10,7 @@ class IN_PROCESS_HANDLER; typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_REQUEST_HANDLER) (IN_PROCESS_HANDLER* pInProcessHandler, void* pvRequestHandlerContext); +typedef VOID(WINAPI * PFN_DISCONNECT_HANDLER) (void *pvManagedHttpContext); typedef BOOL(WINAPI * PFN_SHUTDOWN_HANDLER) (void* pvShutdownHandlerContext); typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_ASYNC_COMPLETION_HANDLER)(void *pvManagedHttpContext, HRESULT hrCompletionStatus, DWORD cbCompletion); @@ -33,6 +34,7 @@ public: SetCallbackHandles( _In_ PFN_REQUEST_HANDLER request_callback, _In_ PFN_SHUTDOWN_HANDLER shutdown_callback, + _In_ PFN_DISCONNECT_HANDLER disconnect_callback, _In_ PFN_ASYNC_COMPLETION_HANDLER managed_context_callback, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext @@ -48,14 +50,14 @@ public: // Executes the .NET Core process void ExecuteApplication(); - + HRESULT LoadManagedApplication(); void QueueStop(); - + void StopIncomingRequests() { @@ -110,7 +112,7 @@ public: private: struct ExecuteClrContext: std::enable_shared_from_this { - ExecuteClrContext(): + ExecuteClrContext(): m_argc(0), m_pProc(nullptr), m_exitCode(0), @@ -121,11 +123,11 @@ private: DWORD m_argc; std::unique_ptr m_argv; hostfxr_main_fn m_pProc; - + int m_exitCode; int m_exceptionCode; }; - + // Thread executing the .NET Core process this might be abandoned in timeout cases std::thread m_clrThread; // Thread tracking the CLR thread, this one is always joined on shutdown @@ -144,6 +146,7 @@ private: VOID* m_ShutdownHandlerContext; PFN_ASYNC_COMPLETION_HANDLER m_AsyncCompletionHandler; + PFN_DISCONNECT_HANDLER m_DisconnectHandler; std::wstring m_dotnetExeKnownLocation; @@ -164,7 +167,7 @@ private: HRESULT SetEnvironmentVariablesOnWorkerProcess(); - + void StopClr(); @@ -175,7 +178,7 @@ private: static void ExecuteClr(const std::shared_ptr &context); - + // Allows to override call to hostfxr_main with custom callback // used in testing inline static hostfxr_main_fn s_fMainCallback = nullptr; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp index 9e7e84d7f1..c8fc89956e 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp @@ -6,6 +6,7 @@ #include "aspnetcore_event.h" #include "IOutputManager.h" #include "ShuttingDownApplication.h" +#include "ntassert.h" ALLOC_CACHE_HANDLER * IN_PROCESS_HANDLER::sm_pAlloc = NULL; @@ -14,6 +15,7 @@ IN_PROCESS_HANDLER::IN_PROCESS_HANDLER( _In_ IHttpContext *pW3Context, _In_ PFN_REQUEST_HANDLER pRequestHandler, _In_ void * pRequestHandlerContext, + _In_ PFN_DISCONNECT_HANDLER pDisconnectHandler, _In_ PFN_ASYNC_COMPLETION_HANDLER pAsyncCompletion ): m_pManagedHttpContext(nullptr), m_requestNotificationStatus(RQ_NOTIFICATION_PENDING), @@ -22,7 +24,8 @@ IN_PROCESS_HANDLER::IN_PROCESS_HANDLER( m_pApplication(std::move(pApplication)), m_pRequestHandler(pRequestHandler), m_pRequestHandlerContext(pRequestHandlerContext), - m_pAsyncCompletionHandler(pAsyncCompletion) + m_pAsyncCompletionHandler(pAsyncCompletion), + m_pDisconnectHandler(pDisconnectHandler) { } @@ -63,7 +66,7 @@ IN_PROCESS_HANDLER::OnExecuteRequestHandler() { return ServerShutdownMessage(); } - + return m_pRequestHandler(this, m_pRequestHandlerContext); } @@ -87,6 +90,7 @@ IN_PROCESS_HANDLER::OnAsyncCompletion( return ServerShutdownMessage(); } + assert(m_pManagedHttpContext != nullptr); // Call the managed handler for async completion. return m_pAsyncCompletionHandler(m_pManagedHttpContext, hrCompletionStatus, cbCompletion); } @@ -97,11 +101,16 @@ REQUEST_NOTIFICATION_STATUS IN_PROCESS_HANDLER::ServerShutdownMessage() const } VOID -IN_PROCESS_HANDLER::TerminateRequest( - bool fClientInitiated -) +IN_PROCESS_HANDLER::NotifyDisconnect() { - UNREFERENCED_PARAMETER(fClientInitiated); + if (m_pApplication->QueryBlockCallbacksIntoManaged() || + m_fManagedRequestComplete) + { + return; + } + + assert(m_pManagedHttpContext != nullptr); + m_pDisconnectHandler(m_pManagedHttpContext); } VOID @@ -110,6 +119,7 @@ IN_PROCESS_HANDLER::IndicateManagedRequestComplete( ) { m_fManagedRequestComplete = TRUE; + m_pManagedHttpContext = nullptr; } VOID diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h index 6089adc3ca..c1fae5dfc7 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h @@ -18,6 +18,7 @@ public: _In_ IHttpContext *pW3Context, _In_ PFN_REQUEST_HANDLER pRequestHandler, _In_ void * pRequestHandlerContext, + _In_ PFN_DISCONNECT_HANDLER m_DisconnectHandler, _In_ PFN_ASYNC_COMPLETION_HANDLER pAsyncCompletion); ~IN_PROCESS_HANDLER() override = default; @@ -35,10 +36,8 @@ public: __override VOID - TerminateRequest( - bool fClientInitiated - ) override; - + NotifyDisconnect() override; + IHttpContext* QueryHttpContext() const { @@ -82,6 +81,6 @@ private: PFN_REQUEST_HANDLER m_pRequestHandler; void* m_pRequestHandlerContext; PFN_ASYNC_COMPLETION_HANDLER m_pAsyncCompletionHandler; - + PFN_DISCONNECT_HANDLER m_pDisconnectHandler; static ALLOC_CACHE_HANDLER * sm_pAlloc; }; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index 27a41da909..2185f975a4 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -16,6 +16,7 @@ register_callbacks( _In_ IN_PROCESS_APPLICATION* pInProcessApplication, _In_ PFN_REQUEST_HANDLER request_handler, _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, + _In_ PFN_DISCONNECT_HANDLER disconnect_handler, _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext @@ -29,6 +30,7 @@ register_callbacks( pInProcessApplication->SetCallbackHandles( request_handler, shutdown_handler, + disconnect_handler, async_completion_handler, pvRequstHandlerContext, pvShutdownHandlerContext @@ -424,6 +426,16 @@ http_disable_buffering( return S_OK; } +EXTERN_C __MIDL_DECLSPEC_DLLEXPORT +HRESULT +http_close_connection( + _In_ IN_PROCESS_HANDLER* pInProcessHandler +) +{ + pInProcessHandler->QueryHttpContext()->GetResponse()->ResetConnection(); + return S_OK; +} + EXTERN_C __MIDL_DECLSPEC_DLLEXPORT HRESULT http_response_set_unknown_header( diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj index df9c00ebb1..3c4ee3b264 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/OutOfProcessRequestHandler.vcxproj @@ -224,7 +224,6 @@ - diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/disconnectcontext.h b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/disconnectcontext.h deleted file mode 100644 index e421fe11e3..0000000000 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/disconnectcontext.h +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -#pragma once - -class ASYNC_DISCONNECT_CONTEXT : public IHttpConnectionStoredContext -{ -public: - ASYNC_DISCONNECT_CONTEXT() - { - m_pHandler = NULL; - } - - VOID - CleanupStoredContext() - { - DBG_ASSERT(m_pHandler == NULL); - delete this; - } - - VOID - NotifyDisconnect() - { - IREQUEST_HANDLER *pInitialValue = (IREQUEST_HANDLER*) - InterlockedExchangePointer((PVOID*)&m_pHandler, NULL); - - if (pInitialValue != NULL) - { - pInitialValue->TerminateRequest(TRUE); - pInitialValue->DereferenceRequestHandler(); - } - } - - VOID - SetHandler( - IREQUEST_HANDLER *pHandler - ) - { - // - // Take a reference on the forwarding handler. - // This reference will be released on either of two conditions: - // - // 1. When the request processing ends, in which case a ResetHandler() - // is called. - // - // 2. When a disconnect notification arrives. - // - // We need to make sure that only one of them ends up dereferencing - // the object. - // - - DBG_ASSERT(pHandler != NULL); - DBG_ASSERT(m_pHandler == NULL); - - pHandler->ReferenceRequestHandler(); - InterlockedExchangePointer((PVOID*)&m_pHandler, pHandler); - } - - VOID - ResetHandler( - VOID - ) - { - IREQUEST_HANDLER *pInitialValue = (IREQUEST_HANDLER*) - InterlockedExchangePointer((PVOID*)&m_pHandler, NULL); - - if (pInitialValue != NULL) - { - pInitialValue->DereferenceRequestHandler(); - } - } - -private: - ~ASYNC_DISCONNECT_CONTEXT() - {} - - IREQUEST_HANDLER * m_pHandler; -}; \ No newline at end of file diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp index 6dd018783b..c43241495d 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.cpp @@ -46,7 +46,8 @@ FORWARDING_HANDLER::FORWARDING_HANDLER( m_fServerResetConn(FALSE), m_cRefs(1), m_pW3Context(pW3Context), - m_pApplication(std::move(pApplication)) + m_pApplication(std::move(pApplication)), + m_fReactToDisconnect(FALSE) { LOG_TRACE(L"FORWARDING_HANDLER::FORWARDING_HANDLER"); @@ -73,7 +74,7 @@ FORWARDING_HANDLER::~FORWARDING_HANDLER( // The m_pServer cleanup would happen afterwards, since there may be a // call pending from SHARED_HANDLER to FORWARDING_HANDLER::SetStatusAndHeaders() // - DBG_ASSERT(m_pDisconnect == NULL); + DBG_ASSERT(!m_fReactToDisconnect); RemoveRequest(); @@ -93,7 +94,6 @@ FORWARDING_HANDLER::OnExecuteRequestHandler() REQUEST_NOTIFICATION_STATUS retVal = RQ_NOTIFICATION_CONTINUE; HRESULT hr = S_OK; BOOL fRequestLocked = FALSE; - BOOL fHandleSet = FALSE; BOOL fFailedToStartKestrel = FALSE; BOOL fSecure = FALSE; HINTERNET hConnect = NULL; @@ -199,31 +199,7 @@ FORWARDING_HANDLER::OnExecuteRequestHandler() goto Failure; } - // Set client disconnect callback contract with IIS - m_pDisconnect = static_cast( - pClientConnection->GetModuleContextContainer()-> - GetConnectionModuleContext(m_pModuleId)); - if (m_pDisconnect == NULL) - { - m_pDisconnect = new ASYNC_DISCONNECT_CONTEXT(); - if (m_pDisconnect == NULL) - { - hr = E_OUTOFMEMORY; - goto Failure; - } - - hr = pClientConnection->GetModuleContextContainer()-> - SetConnectionModuleContext(m_pDisconnect, - m_pModuleId); - DBG_ASSERT(hr != HRESULT_FROM_WIN32(ERROR_ALREADY_ASSIGNED)); - if (FAILED_LOG(hr)) - { - goto Failure; - } - } - - m_pDisconnect->SetHandler(this); - fHandleSet = TRUE; + m_fReactToDisconnect = TRUE; // require lock as client disconnect callback may happen AcquireSRWLockShared(&m_RequestLock); @@ -2705,21 +2681,16 @@ FORWARDING_HANDLER::RemoveRequest( VOID ) { - ASYNC_DISCONNECT_CONTEXT * pDisconnect; - pDisconnect = (ASYNC_DISCONNECT_CONTEXT *)InterlockedExchangePointer((PVOID*)&m_pDisconnect, NULL); - if (pDisconnect != NULL) - { - pDisconnect->ResetHandler(); - pDisconnect = NULL; - } + m_fReactToDisconnect = FALSE; } VOID -FORWARDING_HANDLER::TerminateRequest( - bool fClientInitiated -) +FORWARDING_HANDLER::NotifyDisconnect() { - UNREFERENCED_PARAMETER(fClientInitiated); + if (!m_fReactToDisconnect) + { + return; + } BOOL fLocked = FALSE; if (TlsGetValue(g_dwTlsIndex) != this) @@ -2740,7 +2711,7 @@ FORWARDING_HANDLER::TerminateRequest( if (!m_fHttpHandleInClose) { - m_fClientDisconnected = fClientInitiated; + m_fClientDisconnected = true; } if (fLocked) diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h index a41b86fe50..cb9255e16d 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/forwardinghandler.h @@ -68,9 +68,7 @@ public: StaticTerminate(); VOID - TerminateRequest( - bool fClientInitiated - ); + NotifyDisconnect() override; static void * operator new(size_t size); @@ -220,7 +218,6 @@ private: DWORD m_cMinBufferLimit; ULONGLONG m_cContentLength; WEBSOCKET_HANDLER * m_pWebSocket; - ASYNC_DISCONNECT_CONTEXT * m_pDisconnect; BYTE * m_pEntityBuffer; static const SIZE_T INLINE_ENTITY_BUFFERS = 8; @@ -239,5 +236,5 @@ private: mutable LONG m_cRefs; IHttpContext* m_pW3Context; std::unique_ptr m_pApplication; - HTTP_MODULE_ID m_pModuleId; + bool m_fReactToDisconnect; }; diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h index 83498f3057..0134ac111e 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/stdafx.h @@ -59,7 +59,6 @@ #include "resources.h" #include "aspnetcore_event.h" #include "aspnetcore_msg.h" -#include "disconnectcontext.h" #include "requesthandler_config.h" #include "sttimer.h" diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs index 46cda59046..8ff01c778f 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/DuplexStream.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.Server.IIS.Core { - // TODO redudant file, remove // See https://github.com/aspnet/IISIntegration/issues/426 internal class DuplexStream : Stream { diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs new file mode 100644 index 0000000000..dfc804df42 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/EmptyStream.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class EmptyStream : ReadOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private HttpStreamState _state; + private Exception _error; + + public EmptyStream(IHttpBodyControlFeature bodyControl) + { + _bodyControl = bodyControl; + _state = HttpStreamState.Open; + } + + public override void Flush() + { + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed); + } + + return 0; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return Task.FromResult(0); + } + + public void StopAcceptingReads() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + } + + public void Abort(Exception error = null) + { + // We don't want to throw an ODE until the app func actually completes. + // If the request is aborted, we throw a TaskCanceledException instead, + // unless error is not null, in which case we throw it. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + _error = error; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpRequestStream)); + case HttpStreamState.Aborted: + if (_error != null) + { + ExceptionDispatchInfo.Capture(_error).Throw(); + } + else + { + throw new TaskCanceledException(); + } + break; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs new file mode 100644 index 0000000000..8a9d37b146 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpRequestStream.cs @@ -0,0 +1,160 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpRequestStream : ReadOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private IISHttpContext _body; + private HttpStreamState _state; + private Exception _error; + + public HttpRequestStream(IHttpBodyControlFeature bodyControl) + { + _bodyControl = bodyControl; + _state = HttpStreamState.Closed; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed); + } + + return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override int EndRead(IAsyncResult asyncResult) + { + return ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = ReadAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(task2.Result); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + ValidateState(cancellationToken); + + return ReadAsyncInternal(destination, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + private async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) + { + try + { + return await _body.ReadAsync(buffer, cancellationToken); + } + catch (ConnectionAbortedException ex) + { + throw new TaskCanceledException("The request was aborted", ex); + } + } + + public void StartAcceptingReads(IISHttpContext body) + { + // Only start if not aborted + if (_state == HttpStreamState.Closed) + { + _state = HttpStreamState.Open; + _body = body; + } + } + + public void StopAcceptingReads() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + _body = null; + } + + public void Abort(Exception error = null) + { + // We don't want to throw an ODE until the app func actually completes. + // If the request is aborted, we throw a TaskCanceledException instead, + // unless error is not null, in which case we throw it. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + _error = error; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpRequestStream)); + case HttpStreamState.Aborted: + if (_error != null) + { + ExceptionDispatchInfo.Capture(_error).Throw(); + } + else + { + throw new TaskCanceledException(); + } + break; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs new file mode 100644 index 0000000000..1a04535448 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpResponseStream.cs @@ -0,0 +1,151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpResponseStream : WriteOnlyStream + { + private readonly IHttpBodyControlFeature _bodyControl; + private readonly IISHttpContext _context; + private HttpStreamState _state; + + public HttpResponseStream(IHttpBodyControlFeature bodyControl, IISHttpContext context) + { + _bodyControl = bodyControl; + _context = context; + _state = HttpStreamState.Closed; + } + + public override void Flush() + { + FlushAsync(default(CancellationToken)).GetAwaiter().GetResult(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return _context.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!_bodyControl.AllowSynchronousIO) + { + throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); + } + + WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateState(cancellationToken); + + return _context.WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + ValidateState(cancellationToken); + + return new ValueTask(_httpResponseControl.WriteAsync(source, cancellationToken)); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public void StartAcceptingWrites() + { + // Only start if not aborted + if (_state == HttpStreamState.Closed) + { + _state = HttpStreamState.Open; + } + } + + public void StopAcceptingWrites() + { + // Can't use dispose (or close) as can be disposed too early by user code + // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes + _state = HttpStreamState.Closed; + } + + public void Abort() + { + // We don't want to throw an ODE until the app func actually completes. + if (_state != HttpStreamState.Closed) + { + _state = HttpStreamState.Aborted; + } + } + + private void ValidateState(CancellationToken cancellationToken) + { + switch (_state) + { + case HttpStreamState.Open: + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + break; + case HttpStreamState.Closed: + throw new ObjectDisposedException(nameof(HttpResponseStream), CoreStrings.WritingToResponseBodyAfterResponseCompleted); + case HttpStreamState.Aborted: + if (cancellationToken.IsCancellationRequested) + { + // Aborted state only throws on write if cancellationToken requests it + cancellationToken.ThrowIfCancellationRequested(); + } + break; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs new file mode 100644 index 0000000000..231db99409 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpStreamState.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + enum HttpStreamState + { + Open, + Closed, + Aborted + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs new file mode 100644 index 0000000000..c8b481f948 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/HttpUpgradeStream.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class HttpUpgradeStream : Stream + { + private readonly Stream _requestStream; + private readonly Stream _responseStream; + + public HttpUpgradeStream(Stream requestStream, Stream responseStream) + { + _requestStream = requestStream; + _responseStream = responseStream; + } + + public override bool CanRead + { + get + { + return _requestStream.CanRead; + } + } + + public override bool CanSeek + { + get + { + return _requestStream.CanSeek; + } + } + + public override bool CanTimeout + { + get + { + return _responseStream.CanTimeout || _requestStream.CanTimeout; + } + } + + public override bool CanWrite + { + get + { + return _responseStream.CanWrite; + } + } + + public override long Length + { + get + { + return _requestStream.Length; + } + } + + public override long Position + { + get + { + return _requestStream.Position; + } + set + { + _requestStream.Position = value; + } + } + + public override int ReadTimeout + { + get + { + return _requestStream.ReadTimeout; + } + set + { + _requestStream.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return _responseStream.WriteTimeout; + } + set + { + _responseStream.WriteTimeout = value; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _requestStream.Dispose(); + _responseStream.Dispose(); + } + } + + public override void Flush() + { + _responseStream.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _responseStream.FlushAsync(cancellationToken); + } + + public override void Close() + { + _requestStream.Close(); + _responseStream.Close(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _requestStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _requestStream.EndRead(asyncResult); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _responseStream.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _responseStream.EndWrite(asyncResult); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _requestStream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + return _requestStream.ReadAsync(destination, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _requestStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _responseStream.WriteAsync(buffer, offset, count, cancellationToken); + } + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + return _responseStream.WriteAsync(source, cancellationToken); + } +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override long Seek(long offset, SeekOrigin origin) + { + return _requestStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _requestStream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _requestStream.Read(buffer, offset, count); + } + + public override int ReadByte() + { + return _requestStream.ReadByte(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _responseStream.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + _responseStream.WriteByte(value); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs index 19f65bc900..6e1656a487 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.FeatureCollection.cs @@ -27,7 +27,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IHttpAuthenticationFeature, IServerVariablesFeature, IHttpBufferingFeature, - ITlsConnectionFeature + ITlsConnectionFeature, + IHttpBodyControlFeature { // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation, // then the list of `implementedFeatures` in the generated code project MUST also be updated. @@ -37,6 +38,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private X509Certificate2 _certificate; private List> MaybeExtra; + public void ResetFeatureCollection() { Initialize(); @@ -179,12 +181,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core set => ResponseBody = value; } - CancellationToken IHttpRequestLifetimeFeature.RequestAborted - { - get => RequestAborted; - set => RequestAborted = value; - } - bool IHttpResponseFeature.HasStarted => HasResponseStarted; bool IHttpUpgradeFeature.IsUpgradableRequest => true; @@ -259,19 +255,18 @@ namespace Microsoft.AspNetCore.Server.IIS.Core async Task IHttpUpgradeFeature.UpgradeAsync() { - // TODO fix these exceptions strings if (!((IHttpUpgradeFeature)this).IsUpgradableRequest) { - throw new InvalidOperationException("CoreStrings.CannotUpgradeNonUpgradableRequest"); + throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest); } if (_wasUpgraded) { - throw new InvalidOperationException("CoreStrings.UpgradeCannotBeCalledMultipleTimes"); + throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes); } if (HasResponseStarted) { - throw new InvalidOperationException("CoreStrings.UpgradeCannotBeCalledMultipleTimes"); + throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes); } _wasUpgraded = true; @@ -291,7 +286,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core await InitializeResponse(flushHeaders: true); - return new DuplexStream(RequestBody, ResponseBody); + return _streams.Upgrade(); } Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) @@ -327,10 +322,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); - void IHttpRequestLifetimeFeature.Abort() - { - Abort(); - } + bool IHttpBodyControlFeature.AllowSynchronousIO { get; set; } = true; void IHttpBufferingFeature.DisableRequestBuffering() { diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000..ab46f8df5c --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IHttpRequestLifetimeFeature.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal partial class IISHttpContext : IHttpRequestLifetimeFeature + { + private CancellationTokenSource _abortedCts; + private CancellationToken? _manuallySetRequestAbortToken; + + CancellationToken IHttpRequestLifetimeFeature.RequestAborted + { + get + { + // If a request abort token was previously explicitly set, return it. + if (_manuallySetRequestAbortToken.HasValue) + { + return _manuallySetRequestAbortToken.Value; + } + // Otherwise, get the abort CTS. If we have one, which would mean that someone previously + // asked for the RequestAborted token, simply return its token. If we don't, + // check to see whether we've already aborted, in which case just return an + // already canceled token. Finally, force a source into existence if we still + // don't have one, and return its token. + var cts = _abortedCts; + return + cts != null ? cts.Token : + (_requestAborted == 1) ? new CancellationToken(true) : + RequestAbortedSource.Token; + } + set + { + // Set an abort token, overriding one we create internally. This setter and associated + // field exist purely to support IHttpRequestLifetimeFeature.set_RequestAborted. + _manuallySetRequestAbortToken = value; + } + } + + private CancellationTokenSource RequestAbortedSource + { + get + { + // Get the abort token, lazily-initializing it if necessary. + // Make sure it's canceled if an abort request already came in. + + // EnsureInitialized can return null since _abortedCts is reset to null + // after it's already been initialized to a non-null value. + // If EnsureInitialized does return null, this property was accessed between + // requests so it's safe to return an ephemeral CancellationTokenSource. + var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource()) + ?? new CancellationTokenSource(); + + if (_requestAborted == 1) + { + cts.Cancel(); + } + return cts; + } + } + + void IHttpRequestLifetimeFeature.Abort() + { + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.ReadWrite.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs similarity index 81% rename from src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.ReadWrite.cs rename to src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs index 26315971ef..09c205d8de 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.ReadWrite.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.IO.cs @@ -3,9 +3,9 @@ using System; using System.Buffers; -using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.IIS.Core { @@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private async Task ReadBody() { + Exception error = null; try { while (true) @@ -112,18 +113,24 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } } + catch (ConnectionResetException ex) + { + ConnectionReset(); + error = ex; + } catch (Exception ex) { - _bodyInputPipe.Writer.Complete(ex); + error = ex; } finally { - _bodyInputPipe.Writer.Complete(); + _bodyInputPipe.Writer.Complete(error); } } private async Task WriteBody(bool flush = false) { + Exception error = null; try { while (true) @@ -160,17 +167,51 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } // We want to swallow IO exception and allow app to finish writing + catch (ConnectionResetException) + { + ConnectionReset(); + } catch (Exception ex) { - if (!(ex is IOException)) - { - _bodyOutput.Reader.Complete(ex); - } + error = ex; } finally { - _bodyOutput.Reader.Complete(); + _bodyOutput.Reader.Complete(error); } } + + private void AbortIO() + { + if (Interlocked.CompareExchange(ref _requestAborted, 1, 0) != 0) + { + return; + } + + _bodyOutput.Dispose(); + + try + { + _abortedCts?.Cancel(); + } + catch (Exception) + { + // ignore + } + } + + public void Abort(Exception reason) + { + _bodyOutput.Abort(reason); + _streams.Abort(reason); + NativeMethods.HttpCloseConnection(_pInProcessHandler); + + AbortIO(); + } + + internal void ConnectionReset() + { + AbortIO(); + } } } diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs index ab977c4e82..a9eecba00b 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContext.cs @@ -15,6 +15,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Server.IIS.Core.IO; @@ -32,6 +33,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private readonly IISServerOptions _options; + protected Streams _streams; + private volatile bool _hasResponseStarted; private volatile bool _hasRequestReadingStarted; @@ -62,7 +65,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private const string BasicString = "Basic"; - internal unsafe IISHttpContext(MemoryPool memoryPool, IntPtr pInProcessHandler, IISServerOptions options, IISHttpServer server) + internal unsafe IISHttpContext( + MemoryPool memoryPool, + IntPtr pInProcessHandler, + IISServerOptions options, + IISHttpServer server) : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler)) { _memoryPool = memoryPool; @@ -78,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core public string Path { get; set; } public string QueryString { get; set; } public string RawTarget { get; set; } - public CancellationToken RequestAborted { get; set; } + public bool HasResponseStarted => _hasResponseStarted; public IPAddress RemoteIpAddress { get; set; } public int RemotePort { get; set; } @@ -151,9 +158,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _currentIHttpUpgradeFeature = null; } - RequestBody = new IISHttpRequestBody(this); - ResponseBody = new IISHttpResponseBody(this); + _streams = new Streams(this); + (RequestBody, ResponseBody) = _streams.Start(); var pipe = new Pipe( new PipeOptions( @@ -223,9 +230,9 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } // Client might be disconnected at this point // don't leak the exception - catch (IOException) + catch (ConnectionResetException) { - // ignore + ConnectionReset(); } } @@ -255,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private void ThrowResponseAbortedException() { - throw new ObjectDisposedException("Unhandled application exception", _applicationException); + throw new ObjectDisposedException(CoreStrings.UnhandledApplicationException, _applicationException); } protected Task ProduceEnd() @@ -340,11 +347,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } - public void Abort() - { - // TODO - } - public abstract Task ProcessRequestAsync(); public void OnStarting(Func callback, object state) @@ -459,27 +461,28 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { if (disposing) { - // TODO: dispose managed state (managed objects). _thisHandle.Free(); } + if (WindowsUser?.Identity is WindowsIdentity wi) { wi.Dispose(); } + + _abortedCts?.Dispose(); + disposedValue = true; } } - // This code added to correctly implement the disposable pattern. public override void Dispose() { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(disposing: true); } - private void ThrowResponseAlreadyStartedException(string value) + private void ThrowResponseAlreadyStartedException(string name) { - throw new InvalidOperationException("Response already started"); + throw new InvalidOperationException(CoreStrings.FormatParameterReadOnlyAfterResponseStarted(name)); } private WindowsPrincipal GetWindowsPrincipal() diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs index 8fb2d75363..ec3fc006b2 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpContextOfT.cs @@ -44,6 +44,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } finally { + _streams.Stop(); + if (!HasResponseStarted && _applicationException == null && _onStarting != null) { await FireOnStarting(); @@ -91,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } // Cancel all remaining IO, there might be reads pending if not entire request body was sent by client - AsyncIO.Dispose(); + AsyncIO?.Dispose(); if (_readBodyTask != null) { diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpResponseBody.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpResponseBody.cs deleted file mode 100644 index 76e913bb4b..0000000000 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpResponseBody.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Server.IIS.Core -{ - internal class IISHttpResponseBody : Stream - { - private readonly IISHttpContext _httpContext; - - public IISHttpResponseBody(IISHttpContext httpContext) - { - _httpContext = httpContext; - } - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length => throw new NotSupportedException(); - - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - public override void Flush() - { - FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public unsafe override void Write(byte[] buffer, int offset, int count) - { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - - public override unsafe Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _httpContext.WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _httpContext.FlushAsync(cancellationToken); - } - } -} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs index ddf16f269d..7e540ddd83 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpServer.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core private static readonly NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest; private static readonly NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown; + private static readonly NativeMethods.PFN_DISCONNECT_HANDLER _onDisconnect = OnDisconnect; private static readonly NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion; private IISContextFactory _iisContextFactory; @@ -82,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _httpServerHandle = GCHandle.Alloc(this); _iisContextFactory = new IISContextFactory(_memoryPool, application, _options, this); - _nativeApplication.RegisterCallbacks(_requestHandler, _shutdownHandler, _onAsyncCompletion, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); + _nativeApplication.RegisterCallbacks(_requestHandler, _shutdownHandler, _onDisconnect, _onAsyncCompletion, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); return Task.CompletedTask; } @@ -199,6 +200,22 @@ namespace Microsoft.AspNetCore.Server.IIS.Core return true; } + + private static void OnDisconnect(IntPtr pvManagedHttpContext) + { + IISHttpContext context = null; + try + { + context = (IISHttpContext)GCHandle.FromIntPtr(pvManagedHttpContext).Target; + context.ConnectionReset(); + } + catch (Exception ex) + { + context?.Server._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(OnDisconnect)}."); + } + + } + private static NativeMethods.REQUEST_NOTIFICATION_STATUS OnAsyncCompletion(IntPtr pvManagedHttpContext, int hr, int bytes) { IISHttpContext context = null; diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs index d8544239c5..1be8e888fd 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/IISNativeApplication.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core public void RegisterCallbacks( NativeMethods.PFN_REQUEST_HANDLER requestHandler, NativeMethods.PFN_SHUTDOWN_HANDLER shutdownHandler, + NativeMethods.PFN_DISCONNECT_HANDLER disconnectHandler, NativeMethods.PFN_ASYNC_COMPLETION onAsyncCompletion, IntPtr requestContext, IntPtr shutdownContext) @@ -36,7 +37,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _nativeApplication, requestHandler, shutdownHandler, - onAsyncCompletion, + disconnectHandler, + onAsyncCompletion, requestContext, shutdownContext); } diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs index 4b90abb006..431f5c809a 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/OutputProducer.cs @@ -3,9 +3,11 @@ using System; using System.Buffers; +using System.Diagnostics; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.IIS.Core { @@ -67,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _completed = true; _pipe.Reader.CancelPendingRead(); - _pipe.Writer.Complete(error); + _pipe.Writer.Complete(); } } @@ -77,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { if (_completed) { - throw new ObjectDisposedException("Response is already completed"); + return Task.CompletedTask; } _pipe.Writer.Write(buffer.Span); @@ -119,11 +121,14 @@ namespace Microsoft.AspNetCore.Server.IIS.Core await _flushTcs.Task; cancellationToken.ThrowIfCancellationRequested(); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - _pipe.Writer.Complete(); - _completed = true; - throw; + Abort(new ConnectionAbortedException(CoreStrings.ConnectionOrStreamAbortedByCancellationToken, ex)); + } + catch + { + // A canceled token is the only reason flush should ever throw. + Debug.Assert(false); } } diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs new file mode 100644 index 0000000000..b5bbbf6199 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/ReadOnlyStream.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal abstract class ReadOnlyStream : Stream + { + public override bool CanRead => true; + + public override bool CanWrite => false; + + public override int WriteTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override bool CanSeek => false; + + public override long Length + => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs new file mode 100644 index 0000000000..bc50edd01b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/Streams.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class Streams + { + private static readonly ThrowingWasUpgradedWriteOnlyStream _throwingResponseStream + = new ThrowingWasUpgradedWriteOnlyStream(); + + private readonly IISHttpContext _context; + private readonly HttpResponseStream _response; + private readonly HttpRequestStream _request; + private readonly WrappingStream _upgradeableRequest; + private readonly WrappingStream _upgradeableResponse; + private EmptyStream _emptyRequest; + private Stream _upgradeStream; + + public Streams(IISHttpContext context) + { + _context = context; + _request = new HttpRequestStream(_context); + _response = new HttpResponseStream(_context, _context); + _upgradeableResponse = new WrappingStream(_response); + _upgradeableRequest = new WrappingStream(_request); + } + + public Stream Upgrade() + { + _upgradeStream = new HttpUpgradeStream(_request, _response); + + // causes writes to context.Response.Body to throw + _upgradeableResponse.SetInnerStream(_throwingResponseStream); + + _emptyRequest = new EmptyStream(_context); + + _upgradeableRequest.SetInnerStream(_emptyRequest); + // _upgradeStream always uses _response + return _upgradeStream; + } + + public (Stream request, Stream response) Start() + { + _request.StartAcceptingReads(_context); + _response.StartAcceptingWrites(); + + return (_upgradeableRequest, _upgradeableResponse); + } + + public void Stop() + { + _request.StopAcceptingReads(); + _emptyRequest?.StopAcceptingReads(); + _response.StopAcceptingWrites(); + } + + public void Abort(Exception error) + { + _request.Abort(error); + _emptyRequest?.Abort(error); + _response.Abort(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs new file mode 100644 index 0000000000..3990cd9865 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/ThrowingWasUpgradedWriteOnlyStream.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + public class ThrowingWasUpgradedWriteOnlyStream : WriteOnlyStream + { + public override void Write(byte[] buffer, int offset, int count) + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override void Flush() + => throw new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded); + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => throw new NotSupportedException(); + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs new file mode 100644 index 0000000000..18ae443711 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/WrappingStream.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.IIS.Core +{ + internal class WrappingStream : Stream + { + private Stream _inner; + private bool _disposed; + + public WrappingStream(Stream inner) + { + _inner = inner; + } + + public void SetInnerStream(Stream inner) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(WrappingStream)); + } + + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => _inner.CanSeek; + + public override bool CanWrite => _inner.CanWrite; + + public override bool CanTimeout => _inner.CanTimeout; + + public override long Length => _inner.Length; + + public override long Position + { + get => _inner.Position; + set => _inner.Position = value; + } + + public override int ReadTimeout + { + get => _inner.ReadTimeout; + set => _inner.ReadTimeout = value; + } + + public override int WriteTimeout + { + get => _inner.WriteTimeout; + set => _inner.WriteTimeout = value; + } + + public override void Flush() + => _inner.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _inner.FlushAsync(cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) + => _inner.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _inner.ReadAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP2_1 + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + => _inner.ReadAsync(destination, cancellationToken); +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override int ReadByte() + => _inner.ReadByte(); + + public override long Seek(long offset, SeekOrigin origin) + => _inner.Seek(offset, origin); + + public override void SetLength(long value) + => _inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + => _inner.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _inner.WriteAsync(buffer, offset, count, cancellationToken); + +#if NETCOREAPP2_1 + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + => _inner.WriteAsync(source, cancellationToken); +#elif NETSTANDARD2_0 +#else +#error TFMs need to be updated +#endif + + public override void WriteByte(byte value) + => _inner.WriteByte(value); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => _inner.CopyToAsync(destination, bufferSize, cancellationToken); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + => _inner.BeginRead(buffer, offset, count, callback, state); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + => _inner.BeginWrite(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => _inner.EndRead(asyncResult); + + public override void EndWrite(IAsyncResult asyncResult) + => _inner.EndWrite(asyncResult); + + public override object InitializeLifetimeService() + => _inner.InitializeLifetimeService(); + + public override void Close() + => _inner.Close(); + + public override bool Equals(object obj) + => _inner.Equals(obj); + + public override int GetHashCode() + => _inner.GetHashCode(); + + public override string ToString() + => _inner.ToString(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + _inner.Dispose(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpRequestBody.cs b/src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs similarity index 57% rename from src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpRequestBody.cs rename to src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs index d4f6241f98..9ccf9c9cd1 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/Core/IISHttpRequestBody.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/Core/WriteOnlyStream.cs @@ -8,40 +8,36 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.Server.IIS.Core { - internal class IISHttpRequestBody : Stream + public abstract class WriteOnlyStream : Stream { - private readonly IISHttpContext _httpContext; + public override bool CanRead => false; - public IISHttpRequestBody(IISHttpContext httpContext) + public override bool CanWrite => true; + + public override int ReadTimeout { - _httpContext = httpContext; + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => false; - public override long Length => throw new NotSupportedException(); - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - public override void Flush() + public override long Position { - throw new NotSupportedException(); + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) { - return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + throw new NotSupportedException(); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var memory = new Memory(buffer, offset, count); - - return _httpContext.ReadAsync(memory, cancellationToken); + throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) @@ -53,10 +49,5 @@ namespace Microsoft.AspNetCore.Server.IIS.Core { throw new NotSupportedException(); } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } } } diff --git a/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx new file mode 100644 index 0000000000..d6c2e7b974 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/CoreStrings.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot write to response body after connection has been upgraded. + + + The response has been aborted due to an unhandled application exception. + + + Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + + + IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + + + Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + + + Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + + + Cannot write to the response body, the response has completed. + + + The connection was aborted by the application. + + + The connection or stream was aborted because a write operation was aborted with a CancellationToken. + + + {name} cannot be set because the response has already started. + + diff --git a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs index 906a86cdda..51f4ad448f 100644 --- a/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs +++ b/src/Microsoft.AspNetCore.Server.IIS/NativeMethods.cs @@ -39,6 +39,7 @@ namespace Microsoft.AspNetCore.Server.IIS } public delegate REQUEST_NOTIFICATION_STATUS PFN_REQUEST_HANDLER(IntPtr pInProcessHandler, IntPtr pvRequestContext); + public delegate void PFN_DISCONNECT_HANDLER(IntPtr pvManagedHttpContext); public delegate bool PFN_SHUTDOWN_HANDLER(IntPtr pvRequestContext); public delegate REQUEST_NOTIFICATION_STATUS PFN_ASYNC_COMPLETION(IntPtr pvManagedHttpContext, int hr, int bytes); public delegate REQUEST_NOTIFICATION_STATUS PFN_WEBSOCKET_ASYNC_COMPLETION(IntPtr pInProcessHandler, IntPtr completionInfo, IntPtr pvCompletionContext); @@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Server.IIS private static extern int register_callbacks(IntPtr pInProcessApplication, PFN_REQUEST_HANDLER requestCallback, PFN_SHUTDOWN_HANDLER shutdownCallback, + PFN_DISCONNECT_HANDLER disconnectCallback, PFN_ASYNC_COMPLETION asyncCallback, IntPtr pvRequestContext, IntPtr pvShutdownContext); @@ -130,6 +132,9 @@ namespace Microsoft.AspNetCore.Server.IIS [DllImport(AspNetCoreModuleDll)] private static extern int http_cancel_io(IntPtr pInProcessHandler); + [DllImport(AspNetCoreModuleDll)] + private static extern int http_close_connection(IntPtr pInProcessHandler); + [DllImport(AspNetCoreModuleDll)] private static extern unsafe int http_response_set_unknown_header(IntPtr pInProcessHandler, byte* pszHeaderName, byte* pszHeaderValue, ushort usHeaderValueLength, bool fReplace); @@ -152,11 +157,12 @@ namespace Microsoft.AspNetCore.Server.IIS public static void HttpRegisterCallbacks(IntPtr pInProcessApplication, PFN_REQUEST_HANDLER requestCallback, PFN_SHUTDOWN_HANDLER shutdownCallback, + PFN_DISCONNECT_HANDLER disconnectCallback, PFN_ASYNC_COMPLETION asyncCallback, IntPtr pvRequestContext, IntPtr pvShutdownContext) { - Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, asyncCallback, pvRequestContext, pvShutdownContext)); + Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, disconnectCallback, asyncCallback, pvRequestContext, pvShutdownContext)); } public static unsafe int HttpWriteResponseBytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected) @@ -266,6 +272,11 @@ namespace Microsoft.AspNetCore.Server.IIS return true; } + public static void HttpCloseConnection(IntPtr pInProcessHandler) + { + Validate(http_close_connection(pInProcessHandler)); + } + public static unsafe void HttpResponseSetUnknownHeader(IntPtr pInProcessHandler, byte* pszHeaderName, byte* pszHeaderValue, ushort usHeaderValueLength, bool fReplace) { Validate(http_response_set_unknown_header(pInProcessHandler, pszHeaderName, pszHeaderValue, usHeaderValueLength, fReplace)); diff --git a/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs new file mode 100644 index 0000000000..639480f4a5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IIS/Properties/CoreStrings.Designer.cs @@ -0,0 +1,170 @@ +// +namespace Microsoft.AspNetCore.Server.IIS +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class CoreStrings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Server.IIS.CoreStrings", typeof(CoreStrings).GetTypeInfo().Assembly); + + /// + /// Cannot write to response body after connection has been upgraded. + /// + internal static string ResponseStreamWasUpgraded + { + get => GetString("ResponseStreamWasUpgraded"); + } + + /// + /// Cannot write to response body after connection has been upgraded. + /// + internal static string FormatResponseStreamWasUpgraded() + => GetString("ResponseStreamWasUpgraded"); + + /// + /// The response has been aborted due to an unhandled application exception. + /// + internal static string UnhandledApplicationException + { + get => GetString("UnhandledApplicationException"); + } + + /// + /// The response has been aborted due to an unhandled application exception. + /// + internal static string FormatUnhandledApplicationException() + => GetString("UnhandledApplicationException"); + + /// + /// Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + /// + internal static string CannotUpgradeNonUpgradableRequest + { + get => GetString("CannotUpgradeNonUpgradableRequest"); + } + + /// + /// Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded. + /// + internal static string FormatCannotUpgradeNonUpgradableRequest() + => GetString("CannotUpgradeNonUpgradableRequest"); + + /// + /// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + /// + internal static string UpgradeCannotBeCalledMultipleTimes + { + get => GetString("UpgradeCannotBeCalledMultipleTimes"); + } + + /// + /// IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection. + /// + internal static string FormatUpgradeCannotBeCalledMultipleTimes() + => GetString("UpgradeCannotBeCalledMultipleTimes"); + + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousReadsDisallowed + { + get => GetString("SynchronousReadsDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousReadsDisallowed() + => GetString("SynchronousReadsDisallowed"); + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string SynchronousWritesDisallowed + { + get => GetString("SynchronousWritesDisallowed"); + } + + /// + /// Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + /// + internal static string FormatSynchronousWritesDisallowed() + => GetString("SynchronousWritesDisallowed"); + + /// + /// Cannot write to the response body, the response has completed. + /// + internal static string WritingToResponseBodyAfterResponseCompleted + { + get => GetString("WritingToResponseBodyAfterResponseCompleted"); + } + + /// + /// Cannot write to the response body, the response has completed. + /// + internal static string FormatWritingToResponseBodyAfterResponseCompleted() + => GetString("WritingToResponseBodyAfterResponseCompleted"); + + /// + /// The connection was aborted by the application. + /// + internal static string ConnectionAbortedByApplication + { + get => GetString("ConnectionAbortedByApplication"); + } + + /// + /// The connection was aborted by the application. + /// + internal static string FormatConnectionAbortedByApplication() + => GetString("ConnectionAbortedByApplication"); + + /// + /// The connection or stream was aborted because a write operation was aborted with a CancellationToken. + /// + internal static string ConnectionOrStreamAbortedByCancellationToken + { + get => GetString("ConnectionOrStreamAbortedByCancellationToken"); + } + + /// + /// The connection or stream was aborted because a write operation was aborted with a CancellationToken. + /// + internal static string FormatConnectionOrStreamAbortedByCancellationToken() + => GetString("ConnectionOrStreamAbortedByCancellationToken"); + + /// + /// {name} cannot be set because the response has already started. + /// + internal static string ParameterReadOnlyAfterResponseStarted + { + get => GetString("ParameterReadOnlyAfterResponseStarted"); + } + + /// + /// {name} cannot be set because the response has already started. + /// + internal static string FormatParameterReadOnlyAfterResponseStarted(object name) + => string.Format(CultureInfo.CurrentCulture, GetString("ParameterReadOnlyAfterResponseStarted", "name"), name); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/test/Common.FunctionalTests/AppOfflineTests.cs b/test/Common.FunctionalTests/AppOfflineTests.cs index 53afd34504..c314a9dfab 100644 --- a/test/Common.FunctionalTests/AppOfflineTests.cs +++ b/test/Common.FunctionalTests/AppOfflineTests.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests try { - await runningTask.TimeoutAfterDefault(); + await runningTask.DefaultTimeout(); // if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app // try again diff --git a/test/Common.FunctionalTests/Utilities/Helpers.cs b/test/Common.FunctionalTests/Utilities/Helpers.cs index ccaecbceff..176805cbad 100644 --- a/test/Common.FunctionalTests/Utilities/Helpers.cs +++ b/test/Common.FunctionalTests/Utilities/Helpers.cs @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public static void AssertWorkerProcessStop(this IISDeploymentResult deploymentResult) { var hostProcess = deploymentResult.HostProcess; - Assert.True(hostProcess.WaitForExit((int)TimeoutExtensions.DefaultTimeout.TotalMilliseconds)); + Assert.True(hostProcess.WaitForExit((int)TimeoutExtensions.DefaultTimeoutValue.TotalMilliseconds)); if (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress) { diff --git a/test/Common.Tests/Utilities/TestConnections.cs b/test/Common.Tests/Utilities/TestConnections.cs index 722abc5b96..3364a0e021 100644 --- a/test/Common.Tests/Utilities/TestConnections.cs +++ b/test/Common.Tests/Utilities/TestConnections.cs @@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } public Socket Socket => _socket; + public Stream Stream => _stream; public void Dispose() { diff --git a/test/Common.Tests/Utilities/TimeoutExtensions.cs b/test/Common.Tests/Utilities/TimeoutExtensions.cs index c2d5192dbe..ce7175dff9 100644 --- a/test/Common.Tests/Utilities/TimeoutExtensions.cs +++ b/test/Common.Tests/Utilities/TimeoutExtensions.cs @@ -11,11 +11,16 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting public static class TimeoutExtensions { - public static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(300); + public static TimeSpan DefaultTimeoutValue = TimeSpan.FromSeconds(300); - public static Task TimeoutAfterDefault(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) { - return task.TimeoutAfter(DefaultTimeout, filePath, lineNumber); + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); + } + + public static Task DefaultTimeout(this Task task, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1) + { + return task.TimeoutAfter(DefaultTimeoutValue, filePath, lineNumber); } } } diff --git a/test/IIS.Tests/ClientDisconnectTests.cs b/test/IIS.Tests/ClientDisconnectTests.cs index c1b2c25062..ee55959a93 100644 --- a/test/IIS.Tests/ClientDisconnectTests.cs +++ b/test/IIS.Tests/ClientDisconnectTests.cs @@ -15,15 +15,15 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { [SkipIfHostableWebCoreNotAvailable] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] - public class ClientDisconnectTests : LoggedTest + public class ClientDisconnectTests : StrictTestServerTests { - + [ConditionalFact] public async Task WritesSucceedAfterClientDisconnect() { - var requestStartedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var clientDisconnectedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestCompletedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var clientDisconnectedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); var data = new byte[1024]; using (var testServer = await TestServer.Create( @@ -42,19 +42,59 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests using (var connection = testServer.CreateConnection()) { await SendContentLength1Post(connection); - await requestStartedCompletionSource.Task.TimeoutAfterDefault(); + await requestStartedCompletionSource.Task.DefaultTimeout(); } clientDisconnectedCompletionSource.SetResult(true); - await requestCompletedCompletionSource.Task.TimeoutAfterDefault(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); + } + } + + [ConditionalFact] + public async Task WritesCancelledWhenUsingAbortedToken() + { + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); + + Exception exception = null; + + var data = new byte[1]; + using (var testServer = await TestServer.Create(async ctx => + { + requestStartedCompletionSource.SetResult(true); + try + { + while (true) + { + await ctx.Response.Body.WriteAsync(data, ctx.RequestAborted); + } + } + catch (Exception e) + { + exception = e; + } + + requestCompletedCompletionSource.SetResult(true); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + + await requestStartedCompletionSource.Task.DefaultTimeout(); + } + + await requestCompletedCompletionSource.Task.DefaultTimeout(); + + Assert.IsType(exception); } } [ConditionalFact] public async Task ReadThrowsAfterClientDisconnect() { - var requestStartedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestCompletedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); Exception exception = null; @@ -77,10 +117,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests using (var connection = testServer.CreateConnection()) { await SendContentLength1Post(connection); - await requestStartedCompletionSource.Task.TimeoutAfterDefault(); + await requestStartedCompletionSource.Task.DefaultTimeout(); } - await requestCompletedCompletionSource.Task.TimeoutAfterDefault(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); } Assert.IsType(exception); @@ -90,8 +130,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalFact(Skip = "See: https://github.com/aspnet/IISIntegration/issues/1075")] public async Task WriterThrowsCancelledException() { - var requestStartedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestCompletedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); Exception exception = null; var cancellationTokenSource = new CancellationTokenSource(); @@ -104,7 +144,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { while (true) { - await ctx.Response.Body.WriteAsync(data, cancellationTokenSource.Token); + await ctx.Response.Body.WriteAsync(data, cancellationTokenSource.Token); } } catch (Exception e) @@ -119,9 +159,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { await SendContentLength1Post(connection); - await requestStartedCompletionSource.Task.TimeoutAfterDefault(); + await requestStartedCompletionSource.Task.DefaultTimeout(); cancellationTokenSource.Cancel(); - await requestCompletedCompletionSource.Task.TimeoutAfterDefault(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); } Assert.IsType(exception); @@ -130,8 +170,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalFact(Skip = "See: https://github.com/aspnet/IISIntegration/issues/1075")] public async Task ReaderThrowsCancelledException() { - var requestStartedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestCompletedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartedCompletionSource = CreateTaskCompletionSource(); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); Exception exception = null; var cancellationTokenSource = new CancellationTokenSource(); @@ -155,9 +195,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests using (var connection = testServer.CreateConnection()) { await SendContentLength1Post(connection); - await requestStartedCompletionSource.Task.TimeoutAfterDefault(); + await requestStartedCompletionSource.Task.DefaultTimeout(); cancellationTokenSource.Cancel(); - await requestCompletedCompletionSource.Task.TimeoutAfterDefault(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); } Assert.IsType(exception); } @@ -166,7 +206,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalFact] public async Task ReaderThrowsResetExceptionOnInvalidBody() { - var requestCompletedCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestCompletedCompletionSource = CreateTaskCompletionSource(); Exception exception = null; @@ -195,7 +235,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests "", "ZZZ", ""); - await requestCompletedCompletionSource.Task.TimeoutAfterDefault(); + await requestCompletedCompletionSource.Task.DefaultTimeout(); } } @@ -203,6 +243,27 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal("The client has disconnected", exception.Message); } + [ConditionalFact] + public async Task RequestAbortedIsTrippedWithoutIO() + { + var requestStarted = CreateTaskCompletionSource(); + var requestAborted = CreateTaskCompletionSource(); + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.RequestAborted.Register(() => requestAborted.SetResult(true)); + requestStarted.SetResult(true); + await requestAborted.Task; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await requestStarted.Task; + } + await requestAborted.Task; + } + } private static async Task SendContentLength1Post(TestConnection connection) { await connection.Send( diff --git a/test/IIS.Tests/ConnectionIdFeatureTests.cs b/test/IIS.Tests/ConnectionIdFeatureTests.cs new file mode 100644 index 0000000000..37e69b3a32 --- /dev/null +++ b/test/IIS.Tests/ConnectionIdFeatureTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class HttpBodyControlFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ThrowsOnSyncReadOrWrite() + { + Exception writeException = null; + Exception readException = null; + using (var testServer = await TestServer.Create( + ctx => { + var bodyControl = ctx.Features.Get(); + bodyControl.AllowSynchronousIO = false; + + try + { + ctx.Response.Body.Write(new byte[10]); + } + catch (Exception ex) + { + writeException = ex; + } + + try + { + ctx.Request.Body.Read(new byte[10]); + } + catch (Exception ex) + { + readException = ex; + } + + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.IsType(readException); + Assert.IsType(writeException); + } + } +} diff --git a/test/IIS.Tests/HttpBodyControlFeatureTests.cs b/test/IIS.Tests/HttpBodyControlFeatureTests.cs new file mode 100644 index 0000000000..3d91c445ca --- /dev/null +++ b/test/IIS.Tests/HttpBodyControlFeatureTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ConnectionIdFeatureTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ProvidesConnectionId() + { + string connectionId = null; + using (var testServer = await TestServer.Create(ctx => { + var connectionIdFeature = ctx.Features.Get(); + connectionId = connectionIdFeature.ConnectionId; + return Task.CompletedTask; + }, LoggerFactory)) + { + await testServer.HttpClient.GetStringAsync("/"); + } + + Assert.NotNull(connectionId); + } + } +} diff --git a/test/IIS.Tests/ResponseAbortTests.cs b/test/IIS.Tests/ResponseAbortTests.cs new file mode 100644 index 0000000000..f902748604 --- /dev/null +++ b/test/IIS.Tests/ResponseAbortTests.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [SkipIfHostableWebCoreNotAvailable] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] + public class ResponseAbortTests : StrictTestServerTests + { + [ConditionalFact] + public async Task ClosesWithoutSendingAnything() + { + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ClosesAfterDataSent() + { + var bodyReceived = CreateTaskCompletionSource(); + using (var testServer = await TestServer.Create( + async ctx => { + await ctx.Response.WriteAsync("Abort"); + await ctx.Response.Body.FlushAsync(); + await bodyReceived.Task.DefaultTimeout(); + ctx.Abort(); + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.Receive( + "HTTP/1.1 200 OK", + ""); + await connection.ReceiveHeaders( + "Transfer-Encoding: chunked", + "Server: Microsoft-IIS/10.0"); + + await connection.ReceiveChunk("Abort"); + bodyReceived.SetResult(true); + await connection.WaitForConnectionClose(); + } + } + } + + [ConditionalFact] + public async Task ReadsThrowAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + var a = new byte[10]; + await ctx.Request.Body.ReadAsync(a); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.IsType(exception); + } + + [ConditionalFact] + public async Task WritesNoopAfterAbort() + { + Exception exception = null; + + using (var testServer = await TestServer.Create( + async ctx => { + ctx.Abort(); + try + { + await ctx.Response.Body.WriteAsync(new byte[10]); + } + catch (Exception e) + { + exception = e; + } + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.Null(exception); + } + + [ConditionalFact] + public async Task RequestAbortedIsTrippedAfterAbort() + { + bool tokenAborted = false; + using (var testServer = await TestServer.Create( + ctx => { + ctx.Abort(); + tokenAborted = ctx.RequestAborted.IsCancellationRequested; + return Task.CompletedTask; + }, LoggerFactory)) + { + using (var connection = testServer.CreateConnection()) + { + await SendContentLength1Post(connection); + await connection.WaitForConnectionClose(); + } + } + + Assert.True(tokenAborted); + } + + private static async Task SendContentLength1Post(TestConnection connection) + { + await connection.Send( + "POST / HTTP/1.1", + "Content-Length: 1", + "Host: localhost", + "", + ""); + } + } +} diff --git a/test/IIS.Tests/StrictTestServerTests.cs b/test/IIS.Tests/StrictTestServerTests.cs new file mode 100644 index 0000000000..c8f62beedb --- /dev/null +++ b/test/IIS.Tests/StrictTestServerTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + public class StrictTestServerTests: LoggedTest + { + public override void Dispose() + { + base.Dispose(); + Assert.DoesNotContain(TestSink.Writes, w => w.LogLevel > LogLevel.Information); + } + + protected static TaskCompletionSource CreateTaskCompletionSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + } +} diff --git a/test/WebSites/InProcessWebSite/InProcessWebSite.csproj b/test/WebSites/InProcessWebSite/InProcessWebSite.csproj index 393574d9c8..a8e89bf438 100644 --- a/test/WebSites/InProcessWebSite/InProcessWebSite.csproj +++ b/test/WebSites/InProcessWebSite/InProcessWebSite.csproj @@ -1,12 +1,12 @@ - + + + netcoreapp2.2 true - -