| File: HttpSysListener.cs | Web Access |
| Project: src\aspnetcore\src\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj (Microsoft.AspNetCore.Server.HttpSys) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Diagnostics; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Networking.HttpServer; namespace Microsoft.AspNetCore.Server.HttpSys; /// <summary> /// An HTTP server wrapping the Http.Sys APIs that accepts requests. /// </summary> internal sealed partial class HttpSysListener : IDisposable { // 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 // returned from HttpReceiveClientCertificate when using the // FileCompletionNotificationModes.SkipCompletionPortOnSuccess flag. // This bug was only hit when the buffer passed into HttpReceiveClientCertificate // (1500 bytes initially) is too small for the certificate. // Due to this bug in downlevel operating systems the FileCompletionNotificationModes.SkipCompletionPortOnSuccess // flag is only used on Win8 and later. internal static readonly bool SkipIOCPCallbackOnSuccess = ComNetOS.IsWin8orLater; // Mitigate potential DOS attacks by limiting the number of unknown headers we accept. Numerous header names // with hash collisions will cause the server to consume excess CPU. 1000 headers limits CPU time to under // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; internal MemoryPool<byte> MemoryPool { get; } private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. private readonly ServerSession _serverSession; private readonly UrlGroup _urlGroup; private readonly RequestQueue _requestQueue; private readonly DisconnectListener _disconnectListener; private readonly object _internalLock; public HttpSysListener(HttpSysOptions options, IMemoryPoolFactory<byte> memoryPoolFactory, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); if (!HttpApi.Supported) { throw CreateHttpInitializeFailureException(HttpApi.HttpInitializeStatusCode); } MemoryPool = memoryPoolFactory.Create(new MemoryPoolOptions { Owner = "httpsys" }); Options = options; Logger = loggerFactory.CreateLogger<HttpSysListener>(); _state = State.Stopped; _internalLock = new object(); // V2 initialization sequence: // 1. Create server session // 2. Create url group // 3. Create request queue // 4. Add urls to url group - Done in Start() // 5. Attach request queue to url group - Done in Start() try { _serverSession = new ServerSession(); _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, options.RequestQueueSecurityDescriptor, Logger); _urlGroup = new UrlGroup(_serverSession, _requestQueue, 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(); Log.HttpSysListenerCtorError(Logger, exception); throw; } } internal enum State { Stopped, Started, Disposed, } internal ILogger Logger { get; private set; } internal UrlGroup UrlGroup => _urlGroup; internal RequestQueue RequestQueue => _requestQueue; internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } public bool IsListening { get { return _state == State.Started; } } /// <summary> /// Start accepting incoming requests. /// </summary> public void Start() { CheckDisposed(); Log.ListenerStarting(Logger); // 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 { CheckDisposed(); if (_state == State.Started) { return; } // Always configure the UrlGroup if the intent was to create, only configure the queue if we actually created it if (Options.RequestQueueMode == RequestQueueMode.Create || Options.RequestQueueMode == RequestQueueMode.CreateOrAttach) { Options.Apply(UrlGroup, _requestQueue.Created ? RequestQueue : null); UrlGroup.AttachToQueue(); // All resources are set up correctly. Now add all prefixes. try { Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup); } catch (HttpSysException) { // If an error occurred while adding prefixes, free all resources allocated by previous steps. UrlGroup.DetachFromQueue(); throw; } } _state = State.Started; } catch (Exception exception) { // Make sure the HttpListener instance can't be used if Start() failed. _state = State.Disposed; DisposeInternal(); Log.ListenerStartError(Logger, exception); throw; } } } private void Stop() { try { lock (_internalLock) { CheckDisposed(); if (_state == State.Stopped) { return; } Log.ListenerStopping(Logger); // If this instance registered URL prefixes then remove them before shutting down. if (Options.RequestQueueMode == RequestQueueMode.Create || Options.RequestQueueMode == RequestQueueMode.CreateOrAttach) { Options.UrlPrefixes.UnregisterAllPrefixes(); UrlGroup.DetachFromQueue(); } _state = State.Stopped; } } catch (Exception exception) { Log.ListenerStopError(Logger, exception); throw; } } /// <summary> /// Stop the server and clean up. /// </summary> public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (!disposing) { return; } lock (_internalLock) { try { if (_state == State.Disposed) { return; } Log.ListenerDisposing(Logger); Stop(); DisposeInternal(); } catch (Exception exception) { Log.ListenerDisposeError(Logger, exception); throw; } finally { _state = State.Disposed; } } } 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 static Exception CreateHttpInitializeFailureException(uint httpInitializeStatusCode) { if (httpInitializeStatusCode == ErrorCodes.ERROR_SUCCESS) { // Keep the existing PlatformNotSupportedException behavior when no specific native error is available. return new PlatformNotSupportedException(); } return new HttpSysException( (int)httpInitializeStatusCode, $"HttpInitialize failed with status code 0x{httpInitializeStatusCode:X8}."); } /// <summary> /// Accept a request from the incoming request queue. /// </summary> internal ValueTask<RequestContext> AcceptAsync(AsyncAcceptContext acceptContext) { CheckDisposed(); Debug.Assert(_state != State.Stopped, "Listener has been stopped."); return acceptContext.AcceptAsync(); } internal bool ValidateRequest(NativeRequestContext requestMemory) { try { // Block potential DOS attacks if (requestMemory.UnknownHeaderCount > UnknownHeaderLimit) { SendError(requestMemory.RequestId, StatusCodes.Status400BadRequest, authChallenges: null); return false; } if (!Options.Authentication.AllowAnonymous && !requestMemory.CheckAuthenticated()) { SendError(requestMemory.RequestId, StatusCodes.Status401Unauthorized, AuthenticationManager.GenerateChallenges(Options.Authentication.Schemes)); return false; } } catch (Exception ex) { Log.RequestValidationFailed(Logger, ex, requestMemory.RequestId); return false; } return true; } internal unsafe void SendError(ulong requestId, int httpStatusCode, IList<string>? authChallenges = null) { var httpResponse = new HTTP_RESPONSE_V2(); httpResponse.Base.Version = new() { MajorVersion = 1, MinorVersion = 1 }; using UnmanagedBufferAllocator allocator = new(); byte* bytes; int bytesLength; // Copied from the multi-value headers section of SerializeHeaders if (authChallenges != null && authChallenges.Count > 0) { var knownHeaderInfo = allocator.AllocAsPointer<HTTP_RESPONSE_INFO>(1); httpResponse.pResponseInfo = knownHeaderInfo; knownHeaderInfo[httpResponse.ResponseInfoCount].Type = HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders; knownHeaderInfo[httpResponse.ResponseInfoCount].Length = (uint)sizeof(HTTP_MULTIPLE_KNOWN_HEADERS); var header = allocator.AllocAsPointer<HTTP_MULTIPLE_KNOWN_HEADERS>(1); header->HeaderId = HTTP_HEADER_ID.HttpHeaderWwwAuthenticate; header->Flags = PInvoke.HTTP_RESPONSE_INFO_FLAGS_PRESERVE_ORDER; // The docs say this is for www-auth only. header->KnownHeaderCount = 0; var nativeHeaderValues = allocator.AllocAsPointer<HTTP_KNOWN_HEADER>(authChallenges.Count); header->KnownHeaders = nativeHeaderValues; for (var headerValueIndex = 0; headerValueIndex < authChallenges.Count; headerValueIndex++) { // Add Value var headerValue = authChallenges[headerValueIndex]; bytes = allocator.GetHeaderEncodedBytes(headerValue, out bytesLength); nativeHeaderValues[header->KnownHeaderCount].RawValueLength = checked((ushort)bytesLength); nativeHeaderValues[header->KnownHeaderCount].pRawValue = (PCSTR)bytes; header->KnownHeaderCount++; } knownHeaderInfo[0].pInfo = header; httpResponse.ResponseInfoCount = 1; } httpResponse.Base.StatusCode = checked((ushort)httpStatusCode); var statusDescription = ReasonPhrases.GetReasonPhrase(httpStatusCode); uint statusCode; bytes = allocator.GetHeaderEncodedBytes(statusDescription, out bytesLength); httpResponse.Base.pReason = (PCSTR)bytes; httpResponse.Base.ReasonLength = checked((ushort)bytesLength); const int contentLengthLength = 1; var pContentLength = allocator.AllocAsPointer<byte>(contentLengthLength + 1); pContentLength[0] = (byte)'0'; pContentLength[1] = 0; // null terminator var knownHeaders = httpResponse.Base.Headers.KnownHeaders.AsSpan(); knownHeaders[(int)HttpSysResponseHeader.ContentLength].pRawValue = (PCSTR)pContentLength; knownHeaders[(int)HttpSysResponseHeader.ContentLength].RawValueLength = contentLengthLength; httpResponse.Base.Headers.UnknownHeaderCount = 0; statusCode = PInvoke.HttpSendHttpResponse( _requestQueue.Handle, requestId, 0, httpResponse, null, out _, null, null); if (statusCode != ErrorCodes.ERROR_SUCCESS) { // if we fail to send a 401 something's seriously wrong, abort the request PInvoke.HttpCancelHttpRequest(_requestQueue.Handle, requestId, default); } } private void CheckDisposed() { ObjectDisposedException.ThrowIf(_state == State.Disposed, this); } }