diff --git a/src/Microsoft.Net.Http.Server/AsyncAcceptContext.cs b/src/Microsoft.Net.Http.Server/AsyncAcceptContext.cs index a03a65a..ba34877 100644 --- a/src/Microsoft.Net.Http.Server/AsyncAcceptContext.cs +++ b/src/Microsoft.Net.Http.Server/AsyncAcceptContext.cs @@ -165,7 +165,7 @@ namespace Microsoft.Net.Http.Server retry = false; uint bytesTransferred = 0; statusCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveHttpRequest( - Server.RequestQueueHandle, + Server.RequestQueue.Handle, _nativeRequestContext.RequestBlob->RequestId, (uint)UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, _nativeRequestContext.RequestBlob, diff --git a/src/Microsoft.Net.Http.Server/AuthenticationManager.cs b/src/Microsoft.Net.Http.Server/AuthenticationManager.cs index 6a8d4f6..877e807 100644 --- a/src/Microsoft.Net.Http.Server/AuthenticationManager.cs +++ b/src/Microsoft.Net.Http.Server/AuthenticationManager.cs @@ -100,7 +100,7 @@ namespace Microsoft.Net.Http.Server IntPtr infoptr = new IntPtr(&authInfo); - _server.SetUrlGroupProperty( + _server.UrlGroup.SetProperty( UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty, infoptr, (uint)AuthInfoSize); } diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs b/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs new file mode 100644 index 0000000..478a343 --- /dev/null +++ b/src/Microsoft.Net.Http.Server/NativeInterop/DisconnectListener.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Server +{ + internal class DisconnectListener + { + private readonly ConcurrentDictionary _connectionCancellationTokens + = new ConcurrentDictionary(); + + private readonly RequestQueue _requestQueue; + private readonly ILogger _logger; + + internal DisconnectListener(RequestQueue requestQueue, ILogger logger) + { + _requestQueue = requestQueue; + _logger = logger; + } + + internal CancellationToken GetTokenForConnection(ulong connectionId) + { + try + { + // Create exactly one CancellationToken per connection. + return GetOrCreateDisconnectToken(connectionId); + } + catch (Win32Exception exception) + { + LogHelper.LogException(_logger, "GetConnectionToken", exception); + return CancellationToken.None; + } + } + + private CancellationToken GetOrCreateDisconnectToken(ulong connectionId) + { + // Read case is performance sensitive + ConnectionCancellation cancellation; + if (!_connectionCancellationTokens.TryGetValue(connectionId, out cancellation)) + { + cancellation = GetCreatedConnectionCancellation(connectionId); + } + return cancellation.GetCancellationToken(connectionId); + } + + private ConnectionCancellation GetCreatedConnectionCancellation(ulong connectionId) + { + // Race condition on creation has no side effects + var cancellation = new ConnectionCancellation(this); + return _connectionCancellationTokens.GetOrAdd(connectionId, cancellation); + } + + private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) + { + LogHelper.LogDebug(_logger, "CreateDisconnectToken", "Registering connection for disconnect for connection ID: " + connectionId); + + // Create a nativeOverlapped callback so we can register for disconnect callback + var cts = new CancellationTokenSource(); + + SafeNativeOverlapped nativeOverlapped = null; + var boundHandle = _requestQueue.BoundHandle; + nativeOverlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped( + (errorCode, numBytes, overlappedPtr) => + { + LogHelper.LogDebug(_logger, "CreateDisconnectToken", "http.sys disconnect callback fired for connection ID: " + connectionId); + + // Free the overlapped + nativeOverlapped.Dispose(); + + // Pull the token out of the list and Cancel it. + ConnectionCancellation token; + _connectionCancellationTokens.TryRemove(connectionId, out token); + try + { + cts.Cancel(); + } + catch (AggregateException exception) + { + LogHelper.LogException(_logger, "CreateDisconnectToken Callback", exception); + } + + cts.Dispose(); + }, + null, null)); + + uint statusCode; + try + { + statusCode = UnsafeNclNativeMethods.HttpApi.HttpWaitForDisconnectEx(requestQueueHandle: _requestQueue.Handle, + connectionId: connectionId, reserved: 0, overlapped: nativeOverlapped); + } + catch (Win32Exception exception) + { + statusCode = (uint)exception.NativeErrorCode; + LogHelper.LogException(_logger, "CreateDisconnectToken", exception); + } + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING && + statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + // We got an unknown result so return a None + // TODO: return a canceled token? + return CancellationToken.None; + } + + if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) + { + // IO operation completed synchronously - callback won't be called to signal completion. + // TODO: return a canceled token? + return CancellationToken.None; + } + + return cts.Token; + } + + private class ConnectionCancellation + { + private readonly DisconnectListener _parent; + private volatile bool _initialized; // Must be volatile because initialization is synchronized + private CancellationToken _cancellationToken; + + public ConnectionCancellation(DisconnectListener parent) + { + _parent = parent; + } + + internal CancellationToken GetCancellationToken(ulong connectionId) + { + // Initialized case is performance sensitive + if (_initialized) + { + return _cancellationToken; + } + return InitializeCancellationToken(connectionId); + } + + private CancellationToken InitializeCancellationToken(ulong connectionId) + { + object syncObject = this; +#pragma warning disable 420 // Disable warning about volatile by reference since EnsureInitialized does volatile operations + return LazyInitializer.EnsureInitialized(ref _cancellationToken, ref _initialized, ref syncObject, () => _parent.CreateDisconnectToken(connectionId)); +#pragma warning restore 420 + } + } + } +} diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs b/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs new file mode 100644 index 0000000..6b3ef07 --- /dev/null +++ b/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Server +{ + internal class RequestQueue + { + private static readonly int BindingInfoSize = + Marshal.SizeOf(); + + private readonly UrlGroup _urlGroup; + private readonly ILogger _logger; + + internal RequestQueue(UrlGroup urlGroup, ILogger logger) + { + _urlGroup = urlGroup; + _logger = logger; + + HttpRequestQueueV2Handle requestQueueHandle = null; + var statusCode = UnsafeNclNativeMethods.SafeNetHandles.HttpCreateRequestQueue( + UnsafeNclNativeMethods.HttpApi.Version, null, null, 0, out requestQueueHandle); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + throw new WebListenerException((int)statusCode); + } + + // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS) + if (WebListener.SkipIOCPCallbackOnSuccess && + !UnsafeNclNativeMethods.SetFileCompletionNotificationModes( + requestQueueHandle, + UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess | + UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle)) + { + throw new WebListenerException(Marshal.GetLastWin32Error()); + } + + Handle = requestQueueHandle; + BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle); + } + + internal SafeHandle Handle { get; } + internal ThreadPoolBoundHandle BoundHandle { get; } + + internal unsafe void AttachToUrlGroup() + { + // Set the association between request queue and url group. After this, requests for registered urls will + // get delivered to this request queue. + + var info = new UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO(); + info.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; + info.RequestQueueHandle = Handle.DangerousGetHandle(); + + var infoptr = new IntPtr(&info); + + _urlGroup.SetProperty(UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, + infoptr, (uint)BindingInfoSize); + } + + internal unsafe void DetachFromUrlGroup() + { + // Break the association between request queue and url group. After this, requests for registered urls + // will get 503s. + // Note that this method may be called multiple times (Stop() and then Abort()). This + // is fine since http.sys allows to set HttpServerBindingProperty multiple times for valid + // Url groups. + + var info = new UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO(); + info.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; + info.RequestQueueHandle = IntPtr.Zero; + + var infoptr = new IntPtr(&info); + + _urlGroup.SetProperty(UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, + infoptr, (uint)BindingInfoSize, throwOnError: false); + } + + // The listener must be active for this to work. + internal unsafe void SetLengthLimit(long length) + { + var result = UnsafeNclNativeMethods.HttpApi.HttpSetRequestQueueProperty(Handle, + UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerQueueLengthProperty, + new IntPtr((void*)&length), (uint)Marshal.SizeOf(), 0, IntPtr.Zero); + + if (result != 0) + { + throw new WebListenerException((int)result); + } + } + + public void Dispose() + { + BoundHandle.Dispose(); + Handle.Dispose(); + } + } +} diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/ServerSession.cs b/src/Microsoft.Net.Http.Server/NativeInterop/ServerSession.cs new file mode 100644 index 0000000..1640efb --- /dev/null +++ b/src/Microsoft.Net.Http.Server/NativeInterop/ServerSession.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Diagnostics; + +namespace Microsoft.Net.Http.Server +{ + internal class ServerSession : IDisposable + { + internal unsafe ServerSession() + { + ulong serverSessionId = 0; + var statusCode = UnsafeNclNativeMethods.HttpApi.HttpCreateServerSession( + UnsafeNclNativeMethods.HttpApi.Version, &serverSessionId, 0); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + throw new WebListenerException((int)statusCode); + } + + Debug.Assert(serverSessionId != 0, "Invalid id returned by HttpCreateServerSession"); + + Id = new HttpServerSessionHandle(serverSessionId); + } + + public HttpServerSessionHandle Id { get; private set; } + + public void Dispose() + { + Id.Dispose(); + } + } +} diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/UnsafeNativeMethods.cs b/src/Microsoft.Net.Http.Server/NativeInterop/UnsafeNativeMethods.cs index b4c732e..b5a4162 100644 --- a/src/Microsoft.Net.Http.Server/NativeInterop/UnsafeNativeMethods.cs +++ b/src/Microsoft.Net.Http.Server/NativeInterop/UnsafeNativeMethods.cs @@ -1025,15 +1025,7 @@ namespace Microsoft.Net.Http.Server version.HttpApiMajorVersion = majorVersion; version.HttpApiMinorVersion = minorVersion; - // For pre-Win7 OS versions, we need to check whether http.sys contains the CBT patch. - // We do so by passing HTTP_INITIALIZE_CBT flag to HttpInitialize. If the flag is not - // supported, http.sys is not patched. Note that http.sys will return invalid parameter - // also on Win7, even though it shipped with CBT support. Therefore we must not pass - // the flag on Win7 and later. - uint statusCode = ErrorCodes.ERROR_SUCCESS; - - // on Win7 and later, we don't pass the CBT flag. CBT is always supported. - statusCode = HttpApi.HttpInitialize(version, (uint)HTTP_FLAGS.HTTP_INITIALIZE_SERVER, null); + var statusCode = HttpApi.HttpInitialize(version, (uint)HTTP_FLAGS.HTTP_INITIALIZE_SERVER, null); supported = statusCode == ErrorCodes.ERROR_SUCCESS; } diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs b/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs new file mode 100644 index 0000000..cb4af09 --- /dev/null +++ b/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Server +{ + internal class UrlGroup : IDisposable + { + private ServerSession _serverSession; + private ILogger _logger; + private bool _disposed; + + internal unsafe UrlGroup(ServerSession serverSession, ILogger logger) + { + _serverSession = serverSession; + _logger = logger; + + ulong urlGroupId = 0; + var statusCode = UnsafeNclNativeMethods.HttpApi.HttpCreateUrlGroup( + _serverSession.Id.DangerousGetServerSessionId(), &urlGroupId, 0); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + throw new WebListenerException((int)statusCode); + } + + Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup"); + Id = urlGroupId; + } + + internal ulong Id { get; private set; } + + internal void SetProperty(UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true) + { + Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer"); + + var statusCode = UnsafeNclNativeMethods.HttpApi.HttpSetUrlGroupProperty(Id, property, info, infosize); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + var exception = new WebListenerException((int)statusCode); + LogHelper.LogException(_logger, "SetUrlGroupProperty", exception); + if (throwOnError) + { + throw exception; + } + } + } + + internal void RegisterPrefix(string uriPrefix, int contextId) + { + LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix); + + var statusCode = UnsafeNclNativeMethods.HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) + { + throw new WebListenerException((int)statusCode, string.Format(Resources.Exception_PrefixAlreadyRegistered, uriPrefix)); + } + else + { + throw new WebListenerException((int)statusCode); + } + } + } + + internal bool UnregisterPrefix(string uriPrefix) + { + LogHelper.LogInfo(_logger, "Stop listening on prefix: " + uriPrefix); + + var statusCode = UnsafeNclNativeMethods.HttpApi.HttpRemoveUrlFromUrlGroup(Id, uriPrefix, 0); + + if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND) + { + return false; + } + return true; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + Debug.Assert(Id != 0, "HttpCloseUrlGroup called with invalid url group id"); + + uint statusCode = UnsafeNclNativeMethods.HttpApi.HttpCloseUrlGroup(Id); + + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + LogHelper.LogError(_logger, "CleanupV2Config", "Result: " + statusCode); + } + Id = 0; + } + } +} diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ClientCertLoader.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ClientCertLoader.cs index 8ae8305..62197ab 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ClientCertLoader.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ClientCertLoader.cs @@ -22,14 +22,17 @@ // ----------------------------------------------------------------------- using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Runtime.InteropServices; using System.Security; +using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Microsoft.Net.Http.Server { @@ -39,7 +42,9 @@ namespace Microsoft.Net.Http.Server { private const uint CertBoblSize = 1500; private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(WaitCallback); - + private static readonly int RequestChannelBindStatusSize = + Marshal.SizeOf(); + private SafeNativeOverlapped _overlapped; private byte[] _backingBuffer; private UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* _memoryBlob; @@ -144,7 +149,7 @@ namespace Microsoft.Net.Http.Server return; } _backingBuffer = new byte[checked((int)size)]; - var boundHandle = RequestContext.Server.BoundHandle; + var boundHandle = RequestContext.Server.RequestQueue.BoundHandle; _overlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped(IOCallback, this, _backingBuffer)); _memoryBlob = (UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO*)Marshal.UnsafeAddrOfPinnedArrayElement(_backingBuffer, 0); @@ -362,5 +367,84 @@ namespace Microsoft.Net.Http.Server { get { return _tcs.Task.IsCompleted; } } + + internal static unsafe ChannelBinding GetChannelBindingFromTls(RequestQueue requestQueue, ulong connectionId, ILogger logger) + { + // +128 since a CBT is usually <128 thus we need to call HRCC just once. If the CBT + // is >128 we will get ERROR_MORE_DATA and call again + int size = RequestChannelBindStatusSize + 128; + + Debug.Assert(size >= 0); + + byte[] blob = null; + SafeLocalFreeChannelBinding token = null; + + uint bytesReceived = 0; ; + uint statusCode; + + do + { + blob = new byte[size]; + fixed (byte* blobPtr = blob) + { + // Http.sys team: ServiceName will always be null if + // HTTP_RECEIVE_SECURE_CHANNEL_TOKEN flag is set. + statusCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveClientCertificate( + requestQueue.Handle, + connectionId, + (uint)UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_RECEIVE_SECURE_CHANNEL_TOKEN, + blobPtr, + (uint)size, + &bytesReceived, + SafeNativeOverlapped.Zero); + + if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + int tokenOffset = GetTokenOffsetFromBlob((IntPtr)blobPtr); + int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr); + Debug.Assert(tokenSize < Int32.MaxValue); + + token = SafeLocalFreeChannelBinding.LocalAlloc(tokenSize); + + Marshal.Copy(blob, tokenOffset, token.DangerousGetHandle(), tokenSize); + } + else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA) + { + int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr); + Debug.Assert(tokenSize < Int32.MaxValue); + + size = RequestChannelBindStatusSize + tokenSize; + } + else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER) + { + LogHelper.LogError(logger, "GetChannelBindingFromTls", "Channel binding is not supported."); + return null; // old schannel library which doesn't support CBT + } + else + { + // It's up to the consumer to fail if the missing ChannelBinding matters to them. + LogHelper.LogException(logger, "GetChannelBindingFromTls", new WebListenerException((int)statusCode)); + break; + } + } + } + while (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS); + + return token; + } + + private static int GetTokenOffsetFromBlob(IntPtr blob) + { + Debug.Assert(blob != IntPtr.Zero); + IntPtr tokenPointer = Marshal.ReadIntPtr(blob, (int)Marshal.OffsetOf("ChannelToken")); + Debug.Assert(tokenPointer != IntPtr.Zero); + return (int)IntPtrHelper.Subtract(tokenPointer, blob); + } + + private static int GetTokenSizeFromBlob(IntPtr blob) + { + Debug.Assert(blob != IntPtr.Zero); + return Marshal.ReadInt32(blob, (int)Marshal.OffsetOf("ChannelTokenSize")); + } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/NativeRequestContext.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/NativeRequestContext.cs index cfc100f..4935587 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/NativeRequestContext.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/NativeRequestContext.cs @@ -182,7 +182,7 @@ namespace Microsoft.Net.Http.Server uint newSize = size != 0 ? size : RequestBuffer == null ? DefaultBufferSize : Size; SetBuffer(checked((int)newSize)); - var boundHandle = _acceptResult.Server.BoundHandle; + var boundHandle = _acceptResult.Server.RequestQueue.BoundHandle; _nativeOverlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped(AsyncAcceptContext.IOCallback, _acceptResult, RequestBuffer)); diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs index 3fe3b9a..c0fede1 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestContext.cs @@ -22,9 +22,11 @@ //------------------------------------------------------------------------------ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; +using System.Security.Authentication.ExtendedProtection; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; @@ -84,7 +86,7 @@ namespace Microsoft.Net.Http.Server // We need to be able to dispose of the registrations each request to prevent leaks. if (!_disconnectToken.HasValue) { - var connectionDisconnectToken = _server.RegisterForDisconnectNotification(this); + var connectionDisconnectToken = _server.DisconnectListener.GetTokenForConnection(Request.ConnectionId); if (connectionDisconnectToken.CanBeCanceled) { @@ -117,7 +119,7 @@ namespace Microsoft.Net.Http.Server { get { - return _server.RequestQueueHandle; + return _server.RequestQueue.Handle; } } @@ -167,17 +169,25 @@ namespace Microsoft.Net.Http.Server Response.SendOpaqueUpgrade(); // TODO: Async Request.SwitchToOpaqueMode(); Response.SwitchToOpaqueMode(); - Stream opaqueStream = new OpaqueStream(Request.Body, Response.Body); - return Task.FromResult(opaqueStream); + var opaqueStream = new OpaqueStream(Request.Body, Response.Body); + return Task.FromResult(opaqueStream); } - /* - public bool TryGetChannelBinding(ref ChannelBinding value) + // TODO: Public when needed + internal bool TryGetChannelBinding(ref ChannelBinding value) { - value = Server.GetChannelBinding(Request.ConnectionId, Request.IsSecureConnection); + if (!Request.IsSecureConnection) + { + LogHelper.LogDebug(Logger, "TryGetChannelBinding", "Channel binding requires HTTPS."); + return false; + } + + value = ClientCertLoader.GetChannelBindingFromTls(Server.RequestQueue, Request.ConnectionId, Logger); + + Debug.Assert(value != null, "GetChannelBindingFromTls returned null even though OS supposedly supports Extended Protection"); + LogHelper.LogInfo(Logger, "Channel binding retrieved."); return value != null; } - */ public void Dispose() { diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestStreamAsyncResult.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestStreamAsyncResult.cs index 93ad298..150ccea 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/RequestStreamAsyncResult.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/RequestStreamAsyncResult.cs @@ -64,7 +64,7 @@ namespace Microsoft.Net.Http.Server : this(requestStream, userState, callback) { _dataAlreadyRead = dataAlreadyRead; - var boundHandle = requestStream.RequestContext.Server.BoundHandle; + var boundHandle = requestStream.RequestContext.Server.RequestQueue.BoundHandle; _overlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped(IOCallback, this, buffer)); _pinnedBuffer = (Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset)); diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs index dbb9f9e..12af57f 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStreamAsyncResult.cs @@ -54,7 +54,7 @@ namespace Microsoft.Net.Http.Server CancellationTokenRegistration cancellationRegistration) : this(responseStream, cancellationRegistration) { - var boundHandle = _responseStream.RequestContext.Server.BoundHandle; + var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle; object[] objectsToPin; if (buffer.TotalBytes == 0) @@ -117,7 +117,7 @@ namespace Microsoft.Net.Http.Server long? count, bool chunked, CancellationTokenRegistration cancellationRegistration) : this(responseStream, cancellationRegistration) { - var boundHandle = responseStream.RequestContext.Server.BoundHandle; + var boundHandle = responseStream.RequestContext.Server.RequestQueue.BoundHandle; int bufferSize = 1024 * 64; // TODO: Validate buffer size choice. #if NETSTANDARD1_3 diff --git a/src/Microsoft.Net.Http.Server/TimeoutManager.cs b/src/Microsoft.Net.Http.Server/TimeoutManager.cs index 9f1ba26..91eb95a 100644 --- a/src/Microsoft.Net.Http.Server/TimeoutManager.cs +++ b/src/Microsoft.Net.Http.Server/TimeoutManager.cs @@ -270,7 +270,7 @@ namespace Microsoft.Net.Http.Server IntPtr infoptr = new IntPtr(&timeoutinfo); - _server.SetUrlGroupProperty( + _server.UrlGroup.SetProperty( UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerTimeoutsProperty, infoptr, (uint)TimeoutLimitSize); } diff --git a/src/Microsoft.Net.Http.Server/UrlPrefix.cs b/src/Microsoft.Net.Http.Server/UrlPrefix.cs index fb4e8c0..d09b77d 100644 --- a/src/Microsoft.Net.Http.Server/UrlPrefix.cs +++ b/src/Microsoft.Net.Http.Server/UrlPrefix.cs @@ -151,7 +151,7 @@ namespace Microsoft.Net.Http.Server else { // This means a port was specified but was invalid or empty. - throw new FormatException("Invalid prefix, invalid port speficification: " + prefix); + throw new FormatException("Invalid prefix, invalid port specified: " + prefix); } } else diff --git a/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs b/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs index 4e8ee48..fb64fa6 100644 --- a/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs +++ b/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs @@ -49,7 +49,7 @@ namespace Microsoft.Net.Http.Server var id = _nextId++; if (_webListener.IsListening) { - RegisterPrefix(item.Whole, id); + _webListener.UrlGroup.RegisterPrefix(item.Whole, id); } _prefixes.Add(id, item); } @@ -108,7 +108,7 @@ namespace Microsoft.Net.Http.Server id = pair.Key; if (_webListener.IsListening) { - UnregisterPrefix(pair.Value.Whole); + _webListener.UrlGroup.UnregisterPrefix(pair.Value.Whole); } } } @@ -142,7 +142,7 @@ namespace Microsoft.Net.Http.Server foreach (var pair in _prefixes) { // We'll get this index back on each request and use it to look up the prefix to calculate PathBase. - RegisterPrefix(pair.Value.Whole, pair.Key); + _webListener.UrlGroup.RegisterPrefix(pair.Value.Whole, pair.Key); } } } @@ -155,51 +155,9 @@ namespace Microsoft.Net.Http.Server foreach (var prefix in _prefixes.Values) { // ignore possible failures - UnregisterPrefix(prefix.Whole); + _webListener.UrlGroup.UnregisterPrefix(prefix.Whole); } } } - - private void RegisterPrefix(string uriPrefix, int contextId) - { - LogHelper.LogInfo(_webListener.Logger, "Listening on prefix: " + uriPrefix); - - uint statusCode = - UnsafeNclNativeMethods.HttpApi.HttpAddUrlToUrlGroup( - _webListener.UrlGroupId, - uriPrefix, - (ulong)contextId, - 0); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) - { - throw new WebListenerException((int)statusCode, String.Format(Resources.Exception_PrefixAlreadyRegistered, uriPrefix)); - } - else - { - throw new WebListenerException((int)statusCode); - } - } - } - - private bool UnregisterPrefix(string uriPrefix) - { - uint statusCode = 0; - LogHelper.LogInfo(_webListener.Logger, "Stop listening on prefix: " + uriPrefix); - - statusCode = - UnsafeNclNativeMethods.HttpApi.HttpRemoveUrlFromUrlGroup( - _webListener.UrlGroupId, - uriPrefix, - 0); - - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND) - { - return false; - } - return true; - } } } \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Server/WebListener.cs b/src/Microsoft.Net.Http.Server/WebListener.cs index 23f48fc..8a7744f 100644 --- a/src/Microsoft.Net.Http.Server/WebListener.cs +++ b/src/Microsoft.Net.Http.Server/WebListener.cs @@ -22,14 +22,10 @@ //------------------------------------------------------------------------------ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using System.Security.Authentication.ExtendedProtection; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -41,10 +37,6 @@ namespace Microsoft.Net.Http.Server public sealed class WebListener : IDisposable { private const long DefaultRequestQueueLength = 1000; // Http.sys default. - private static readonly int RequestChannelBindStatusSize = - Marshal.SizeOf(); - private static readonly int BindingInfoSize = - Marshal.SizeOf(); // Win8# 559317 fixed a bug in Http.sys's HttpReceiveClientCertificate method. // Without this fix IOCP callbacks were not being called although ERROR_IO_PENDING was @@ -61,20 +53,17 @@ namespace Microsoft.Net.Http.Server // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; - private readonly ConcurrentDictionary _connectionCancellationTokens; - private ILogger _logger; - private SafeHandle _requestQueueHandle; - private ThreadPoolBoundHandle _boundHandle; private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. private bool _ignoreWriteExceptions; - private HttpServerSessionHandle _serverSessionHandle; - private ulong _urlGroupId; + private ServerSession _serverSession; + private UrlGroup _urlGroup; + private RequestQueue _requestQueue; private TimeoutManager _timeoutManager; private AuthenticationManager _authManager; - private bool _v2Initialized; + private DisconnectListener _disconnectListener; private object _internalLock; @@ -108,7 +97,33 @@ namespace Microsoft.Net.Http.Server _urlPrefixes = new UrlPrefixCollection(this); _timeoutManager = new TimeoutManager(this); _authManager = new AuthenticationManager(this); - _connectionCancellationTokens = new ConcurrentDictionary(); + + // V2 initialization sequence: + // 1. Create server session + // 2. Create url group + // 3. Create request queue - Done in Start() + // 4. Add urls to url group - Done in Start() + // 5. Attach request queue to url group - Done in Start() + + try + { + _serverSession = new ServerSession(); + + _urlGroup = new UrlGroup(_serverSession, _logger); + + _requestQueue = new RequestQueue(_urlGroup, _logger); + + _disconnectListener = new DisconnectListener(_requestQueue, _logger); + } + catch (Exception exception) + { + // If Url group or request queue creation failed, close server session before throwing. + _requestQueue?.Dispose(); + _urlGroup?.Dispose(); + _serverSession?.Dispose(); + LogHelper.LogException(_logger, ".Ctor", exception); + throw; + } } internal enum State @@ -134,22 +149,19 @@ namespace Microsoft.Net.Http.Server set { _bufferResponses = value; } } - internal SafeHandle RequestQueueHandle + internal UrlGroup UrlGroup { - get - { - return _requestQueueHandle; - } + get { return _urlGroup; } } - internal ThreadPoolBoundHandle BoundHandle + internal RequestQueue RequestQueue { - get { return _boundHandle; } + get { return _requestQueue; } } - internal ulong UrlGroupId + internal DisconnectListener DisconnectListener { - get { return _urlGroupId; } + get { return _disconnectListener; } } /// @@ -157,46 +169,27 @@ namespace Microsoft.Net.Http.Server /// public TimeoutManager TimeoutManager { - get - { - ValidateV2Property(); - Debug.Assert(_timeoutManager != null, "Timeout manager is not assigned"); - return _timeoutManager; - } + get { return _timeoutManager; } } public AuthenticationManager AuthenticationManager { - get - { - ValidateV2Property(); - Debug.Assert(_authManager != null, "Auth manager is not assigned"); - return _authManager; - } + get { return _authManager; } } internal static bool IsSupported { - get - { - return UnsafeNclNativeMethods.HttpApi.Supported; - } + get { return UnsafeNclNativeMethods.HttpApi.Supported; } } public bool IsListening { - get - { - return _state == State.Started; - } + get { return _state == State.Started; } } internal bool IgnoreWriteExceptions { - get - { - return _ignoreWriteExceptions; - } + get { return _ignoreWriteExceptions; } set { CheckDisposed(); @@ -210,6 +203,7 @@ namespace Microsoft.Net.Http.Server /// public void SetRequestQueueLimit(long limit) { + CheckDisposed(); if (limit <= 0) { throw new ArgumentOutOfRangeException("limit", limit, string.Empty); @@ -221,140 +215,18 @@ namespace Microsoft.Net.Http.Server } _requestQueueLength = limit; - - SetRequestQueueLimit(); - } - - private unsafe void SetRequestQueueLimit() - { - // The listener must be active for this to work. Call from Start after activating. - if (!IsListening || !_requestQueueLength.HasValue) - { - return; - } - - long length = _requestQueueLength.Value; - uint result = UnsafeNclNativeMethods.HttpApi.HttpSetRequestQueueProperty(_requestQueueHandle, - UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerQueueLengthProperty, - new IntPtr((void*)&length), (uint)Marshal.SizeOf(), 0, IntPtr.Zero); - - if (result != 0) - { - throw new WebListenerException((int)result); - } - } - - private void ValidateV2Property() - { - // Make sure that calling CheckDisposed and SetupV2Config is an atomic operation. This - // avoids race conditions if the listener is aborted/closed after CheckDisposed(), but - // before SetupV2Config(). - lock (_internalLock) - { - CheckDisposed(); - SetupV2Config(); - } - } - - internal void SetUrlGroupProperty(UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize) - { - ValidateV2Property(); - uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; - - Debug.Assert(_urlGroupId != 0, "SetUrlGroupProperty called with invalid url group id"); - Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer"); - - // Set the url group property using Http Api. - - statusCode = UnsafeNclNativeMethods.HttpApi.HttpSetUrlGroupProperty( - _urlGroupId, property, info, infosize); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - WebListenerException exception = new WebListenerException((int)statusCode); - LogHelper.LogException(_logger, "SetUrlGroupProperty", exception); - throw exception; - } - } - - private IntPtr DangerousGetHandle() - { - return _requestQueueHandle.DangerousGetHandle(); - } - - private unsafe void SetupV2Config() - { - uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; - ulong id = 0; - - // If we have already initialized V2 config, then nothing to do. - - if (_v2Initialized) - { - return; - } - - // V2 initialization sequence: - // 1. Create server session - // 2. Create url group - // 3. Create request queue - Done in Start() - // 4. Add urls to url group - Done in Start() - // 5. Attach request queue to url group - Done in Start() - - try - { - statusCode = UnsafeNclNativeMethods.HttpApi.HttpCreateServerSession( - UnsafeNclNativeMethods.HttpApi.Version, &id, 0); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - throw new WebListenerException((int)statusCode); - } - - Debug.Assert(id != 0, "Invalid id returned by HttpCreateServerSession"); - - _serverSessionHandle = new HttpServerSessionHandle(id); - - id = 0; - statusCode = UnsafeNclNativeMethods.HttpApi.HttpCreateUrlGroup( - _serverSessionHandle.DangerousGetServerSessionId(), &id, 0); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - throw new WebListenerException((int)statusCode); - } - - Debug.Assert(id != 0, "Invalid id returned by HttpCreateUrlGroup"); - _urlGroupId = id; - - _v2Initialized = true; - } - catch (Exception exception) - { - // If V2 initialization fails, we mark object as unusable. - _state = State.Disposed; - // If Url group or request queue creation failed, close server session before throwing. - if (_serverSessionHandle != null) - { - _serverSessionHandle.Dispose(); - } - LogHelper.LogException(_logger, "SetupV2Config", exception); - throw; - } + _requestQueue.SetLengthLimit(_requestQueueLength.Value); } public void Start() { CheckDisposed(); - // TODO: _logger = LogHelper.CreateLogger(loggerFactory, typeof(OwinWebListener)); LogHelper.LogInfo(_logger, "Start"); - // Make sure there are no race conditions between Start/Stop/Abort/Close/Dispose and - // calls to SetupV2Config: Start needs to setup all resources (esp. in V2 where besides - // the request handle, there is also a server session and a Url group. Abort/Stop must - // not interfere while Start is allocating those resources. The lock also makes sure - // all methods changing state can read and change the state in an atomic way. + // Make sure there are no race conditions between Start/Stop/Abort/Close/Dispose. + // Start needs to setup all resources. Abort/Stop must not interfere while Start is + // allocating those resources. lock (_internalLock) { try @@ -365,12 +237,7 @@ namespace Microsoft.Net.Http.Server return; } - // SetupV2Config() is not called in the ctor, because it may throw. This would - // be a regression since in v1 the ctor never threw. Besides, ctors should do - // minimal work according to the framework design guidelines. - SetupV2Config(); - CreateRequestQueueHandle(); - AttachRequestQueueToUrlGroup(); + _requestQueue.AttachToUrlGroup(); // All resources are set up correctly. Now add all prefixes. try @@ -380,99 +247,24 @@ namespace Microsoft.Net.Http.Server catch (WebListenerException) { // If an error occurred while adding prefixes, free all resources allocated by previous steps. - DetachRequestQueueFromUrlGroup(); + _requestQueue.DetachFromUrlGroup(); throw; } _state = State.Started; - - SetRequestQueueLimit(); } catch (Exception exception) { // Make sure the HttpListener instance can't be used if Start() failed. _state = State.Disposed; - CloseRequestQueueHandle(); - CleanupV2Config(); + DisposeInternal(); LogHelper.LogException(_logger, "Start", exception); throw; } } } - private void CleanupV2Config() - { - // If we never setup V2, just return. - if (!_v2Initialized) - { - return; - } - - // V2 stopping sequence: - // 1. Detach request queue from url group - Done in Stop()/Abort() - // 2. Remove urls from url group - Done in Stop() - // 3. Close request queue - Done in Stop()/Abort() - // 4. Close Url group. - // 5. Close server session. - - Debug.Assert(_urlGroupId != 0, "HttpCloseUrlGroup called with invalid url group id"); - - uint statusCode = UnsafeNclNativeMethods.HttpApi.HttpCloseUrlGroup(_urlGroupId); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - LogHelper.LogError(_logger, "CleanupV2Config", "Result: " + statusCode); - } - _urlGroupId = 0; - - Debug.Assert(_serverSessionHandle != null, "ServerSessionHandle is null in CloseV2Config"); - Debug.Assert(!_serverSessionHandle.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); - - _serverSessionHandle.Dispose(); - } - - private unsafe void AttachRequestQueueToUrlGroup() - { - // Set the association between request queue and url group. After this, requests for registered urls will - // get delivered to this request queue. - - UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO info = new UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO(); - info.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; - info.RequestQueueHandle = DangerousGetHandle(); - - IntPtr infoptr = new IntPtr(&info); - - SetUrlGroupProperty(UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, - infoptr, (uint)BindingInfoSize); - } - - private unsafe void DetachRequestQueueFromUrlGroup() - { - Debug.Assert(_urlGroupId != 0, "DetachRequestQueueFromUrlGroup can't detach using Url group id 0."); - - // Break the association between request queue and url group. After this, requests for registered urls - // will get 503s. - // Note that this method may be called multiple times (Stop() and then Abort()). This - // is fine since http.sys allows to set HttpServerBindingProperty multiple times for valid - // Url groups. - - UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO info = new UnsafeNclNativeMethods.HttpApi.HTTP_BINDING_INFO(); - info.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; - info.RequestQueueHandle = IntPtr.Zero; - - IntPtr infoptr = new IntPtr(&info); - - uint statusCode = UnsafeNclNativeMethods.HttpApi.HttpSetUrlGroupProperty(_urlGroupId, - UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerBindingProperty, - infoptr, (uint)BindingInfoSize); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - LogHelper.LogError(_logger, "DetachRequestQueueFromUrlGroup", "Result: " + statusCode); - } - } - - internal void Stop() + private void Stop() { try { @@ -488,13 +280,7 @@ namespace Microsoft.Net.Http.Server _state = State.Stopped; - DetachRequestQueueFromUrlGroup(); - - // Even though it would be enough to just detach the request queue in v2, in order to - // keep app compat with earlier versions of the framework, we need to close the request queue. - // This will make sure that pending GetContext() calls will complete and throw an exception. Just - // detaching the url group from the request queue would not cause GetContext() to return. - CloseRequestQueueHandle(); + _requestQueue.DetachFromUrlGroup(); } } catch (Exception exception) @@ -504,46 +290,6 @@ namespace Microsoft.Net.Http.Server } } - private unsafe void CreateRequestQueueHandle() - { - uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; - - HttpRequestQueueV2Handle requestQueueHandle = null; - statusCode = - UnsafeNclNativeMethods.SafeNetHandles.HttpCreateRequestQueue( - UnsafeNclNativeMethods.HttpApi.Version, null, null, 0, out requestQueueHandle); - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - throw new WebListenerException((int)statusCode); - } - - // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS) - if (SkipIOCPCallbackOnSuccess && - !UnsafeNclNativeMethods.SetFileCompletionNotificationModes( - requestQueueHandle, - UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess | - UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle)) - { - throw new WebListenerException(Marshal.GetLastWin32Error()); - } - - _requestQueueHandle = requestQueueHandle; - _boundHandle = ThreadPoolBoundHandle.BindHandle(_requestQueueHandle); - } - - private unsafe void CloseRequestQueueHandle() - { - if ((_requestQueueHandle != null) && (!_requestQueueHandle.IsInvalid)) - { - _requestQueueHandle.Dispose(); - } - if (_boundHandle != null) - { - _boundHandle.Dispose(); - } - } - /// /// Stop the server and clean up. /// @@ -571,7 +317,7 @@ namespace Microsoft.Net.Http.Server LogHelper.LogInfo(_logger, "Dispose"); Stop(); - CleanupV2Config(); + DisposeInternal(); } catch (Exception exception) { @@ -585,6 +331,25 @@ namespace Microsoft.Net.Http.Server } } + private void DisposeInternal() + { + // V2 stopping sequence: + // 1. Detach request queue from url group - Done in Stop()/Abort() + // 2. Remove urls from url group - Done in Stop() + // 3. Close request queue - Done in Stop()/Abort() + // 4. Close Url group. + // 5. Close server session. + + _requestQueue.Dispose(); + + _urlGroup.Dispose(); + + Debug.Assert(_serverSession != null, "ServerSessionHandle is null in CloseV2Config"); + Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); + + _serverSession.Dispose(); + } + internal unsafe bool ValidateRequest(NativeRequestContext requestMemory) { // Block potential DOS attacks @@ -624,7 +389,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { - // someother bad error, possible(?) return values are: + // some other bad error, possible(?) return values are: // ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED asyncResult.Dispose(); throw new WebListenerException((int)statusCode); @@ -639,101 +404,6 @@ namespace Microsoft.Net.Http.Server return asyncResult.Task; } - internal CancellationToken RegisterForDisconnectNotification(RequestContext requestContext) - { - try - { - // Create exactly one CancellationToken per connection. - ulong connectionId = requestContext.Request.ConnectionId; - return GetConnectionCancellation(connectionId); - } - catch (Win32Exception exception) - { - LogHelper.LogException(_logger, "RegisterForDisconnectNotification", exception); - return CancellationToken.None; - } - } - - private CancellationToken GetConnectionCancellation(ulong connectionId) - { - // Read case is performance sensitive - ConnectionCancellation cancellation; - if (!_connectionCancellationTokens.TryGetValue(connectionId, out cancellation)) - { - cancellation = GetCreatedConnectionCancellation(connectionId); - } - return cancellation.GetCancellationToken(connectionId); - } - - private ConnectionCancellation GetCreatedConnectionCancellation(ulong connectionId) - { - // Race condition on creation has no side effects - ConnectionCancellation cancellation = new ConnectionCancellation(this); - return _connectionCancellationTokens.GetOrAdd(connectionId, cancellation); - } - - private unsafe CancellationToken CreateDisconnectToken(ulong connectionId) - { - // Debug.WriteLine("Server: Registering connection for disconnect for connection ID: " + connectionId); - - // Create a nativeOverlapped callback so we can register for disconnect callback - var cts = new CancellationTokenSource(); - - SafeNativeOverlapped nativeOverlapped = null; - nativeOverlapped = new SafeNativeOverlapped(_boundHandle, _boundHandle.AllocateNativeOverlapped( - (errorCode, numBytes, overlappedPtr) => - { - // Debug.WriteLine("Server: http.sys disconnect callback fired for connection ID: " + connectionId); - - // Free the overlapped - nativeOverlapped.Dispose(); - - // Pull the token out of the list and Cancel it. - ConnectionCancellation token; - _connectionCancellationTokens.TryRemove(connectionId, out token); - try - { - cts.Cancel(); - } - catch (AggregateException exception) - { - LogHelper.LogException(_logger, "CreateDisconnectToken::Disconnected", exception); - } - - cts.Dispose(); - }, - null, null)); - - uint statusCode; - try - { - statusCode = UnsafeNclNativeMethods.HttpApi.HttpWaitForDisconnectEx(requestQueueHandle: _requestQueueHandle, - connectionId: connectionId, reserved: 0, overlapped: nativeOverlapped); - } - catch (Win32Exception exception) - { - statusCode = (uint)exception.NativeErrorCode; - LogHelper.LogException(_logger, "CreateDisconnectToken", exception); - } - - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING && - statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - // We got an unknown result so return a None - // TODO: return a canceled token? - return CancellationToken.None; - } - - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) - { - // IO operation completed synchronously - callback won't be called to signal completion. - // TODO: return a canceled token? - return CancellationToken.None; - } - - return cts.Token; - } - private unsafe void SendError(ulong requestId, HttpStatusCode httpStatusCode, IList authChallenges) { UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 httpResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2(); @@ -809,7 +479,7 @@ namespace Microsoft.Net.Http.Server statusCode = UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse( - _requestQueueHandle, + _requestQueue.Handle, requestId, 0, &httpResponse, @@ -824,7 +494,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) { // if we fail to send a 401 something's seriously wrong, abort the request - RequestContext.CancelRequest(_requestQueueHandle, requestId); + RequestContext.CancelRequest(_requestQueue.Handle, requestId); } } finally @@ -842,100 +512,6 @@ namespace Microsoft.Net.Http.Server } } - private static int GetTokenOffsetFromBlob(IntPtr blob) - { - Debug.Assert(blob != IntPtr.Zero); - IntPtr tokenPointer = Marshal.ReadIntPtr(blob, (int)Marshal.OffsetOf("ChannelToken")); - Debug.Assert(tokenPointer != IntPtr.Zero); - return (int)IntPtrHelper.Subtract(tokenPointer, blob); - } - - private static int GetTokenSizeFromBlob(IntPtr blob) - { - Debug.Assert(blob != IntPtr.Zero); - return Marshal.ReadInt32(blob, (int)Marshal.OffsetOf("ChannelTokenSize")); - } - - internal ChannelBinding GetChannelBinding(ulong connectionId, bool isSecureConnection) - { - if (!isSecureConnection) - { - LogHelper.LogInfo(_logger, "Channel binding is not supported for HTTP."); - return null; - } - - ChannelBinding result = GetChannelBindingFromTls(connectionId); - - Debug.Assert(result != null, "GetChannelBindingFromTls returned null even though OS supposedly supports Extended Protection"); - LogHelper.LogInfo(_logger, "Channel binding retrieved."); - return result; - } - - private unsafe ChannelBinding GetChannelBindingFromTls(ulong connectionId) - { - // +128 since a CBT is usually <128 thus we need to call HRCC just once. If the CBT - // is >128 we will get ERROR_MORE_DATA and call again - int size = RequestChannelBindStatusSize + 128; - - Debug.Assert(size >= 0); - - byte[] blob = null; - SafeLocalFreeChannelBinding token = null; - - uint bytesReceived = 0; - uint statusCode; - - do - { - blob = new byte[size]; - fixed (byte* blobPtr = blob) - { - // Http.sys team: ServiceName will always be null if - // HTTP_RECEIVE_SECURE_CHANNEL_TOKEN flag is set. - statusCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveClientCertificate( - RequestQueueHandle, - connectionId, - (uint)UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_RECEIVE_SECURE_CHANNEL_TOKEN, - blobPtr, - (uint)size, - &bytesReceived, - SafeNativeOverlapped.Zero); - - if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) - { - int tokenOffset = GetTokenOffsetFromBlob((IntPtr)blobPtr); - int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr); - Debug.Assert(tokenSize < Int32.MaxValue); - - token = SafeLocalFreeChannelBinding.LocalAlloc(tokenSize); - - Marshal.Copy(blob, tokenOffset, token.DangerousGetHandle(), tokenSize); - } - else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA) - { - int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr); - Debug.Assert(tokenSize < Int32.MaxValue); - - size = RequestChannelBindStatusSize + tokenSize; - } - else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER) - { - LogHelper.LogError(_logger, "GetChannelBindingFromTls", "Channel binding is not supported."); - return null; // old schannel library which doesn't support CBT - } - else - { - // It's up to the consumer to fail if the missing ChannelBinding matters to them. - LogHelper.LogException(_logger, "GetChannelBindingFromTls", new WebListenerException((int)statusCode)); - break; - } - } - } - while (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS); - - return token; - } - internal void CheckDisposed() { if (_state == State.Disposed) @@ -943,35 +519,5 @@ namespace Microsoft.Net.Http.Server throw new ObjectDisposedException(this.GetType().FullName); } } - - private class ConnectionCancellation - { - private readonly WebListener _parent; - private volatile bool _initialized; // Must be volatile because initialization is synchronized - private CancellationToken _cancellationToken; - - public ConnectionCancellation(WebListener parent) - { - _parent = parent; - } - - internal CancellationToken GetCancellationToken(ulong connectionId) - { - // Initialized case is performance sensitive - if (_initialized) - { - return _cancellationToken; - } - return InitializeCancellationToken(connectionId); - } - - private CancellationToken InitializeCancellationToken(ulong connectionId) - { - object syncObject = this; -#pragma warning disable 420 // Disable warning about volatile by reference since EnsureInitialized does volatile operations - return LazyInitializer.EnsureInitialized(ref _cancellationToken, ref _initialized, ref syncObject, () => _parent.CreateDisconnectToken(connectionId)); -#pragma warning restore 420 - } - } } } diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs index 6bfa2f0..4073056 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ResponseCachingTests.cs @@ -787,7 +787,7 @@ namespace Microsoft.Net.Http.Server // Responses can be cached for requests with cache-control: no-cache. // http://tools.ietf.org/html/rfc7234#section-5.2.1.4 - [Fact] + [Fact(Skip = "https://github.com/aspnet/WebListener/issues/210")] public async Task Caching_RequestCacheControlNoCache_Cached() { string address;