diff --git a/samples/HelloWorld/Program.cs b/samples/HelloWorld/Program.cs index 6ec6112781..dac0e302bd 100644 --- a/samples/HelloWorld/Program.cs +++ b/samples/HelloWorld/Program.cs @@ -16,9 +16,11 @@ namespace HelloWorld public static async Task Run(string[] args) { - using (WebListener listener = new WebListener()) + var settings = new WebListenerSettings(); + settings.UrlPrefixes.Add("http://localhost:8080"); + + using (WebListener listener = new WebListener(settings)) { - listener.UrlPrefixes.Add("http://localhost:8080"); listener.Start(); Console.WriteLine("Running..."); diff --git a/samples/HotAddSample/Startup.cs b/samples/HotAddSample/Startup.cs index 1fa57b5606..089b14d01d 100644 --- a/samples/HotAddSample/Startup.cs +++ b/samples/HotAddSample/Startup.cs @@ -16,7 +16,7 @@ namespace HotAddSample { loggerfactory.AddConsole(LogLevel.Information); - var addresses = app.ServerFeatures.Get().UrlPrefixes; + var addresses = app.ServerFeatures.Get().Settings.UrlPrefixes; addresses.Add("http://localhost:12346/pathBase/"); app.Use(async (context, next) => diff --git a/samples/SelfHostServer/Startup.cs b/samples/SelfHostServer/Startup.cs index e85fff80f9..c6a9fecf72 100644 --- a/samples/SelfHostServer/Startup.cs +++ b/samples/SelfHostServer/Startup.cs @@ -19,8 +19,8 @@ namespace SelfHostServer // Server options can be configured here instead of in Main. services.Configure(options => { - options.Listener.AuthenticationManager.AuthenticationSchemes = AuthenticationSchemes.None; - options.Listener.AuthenticationManager.AllowAnonymous = true; + options.ListenerSettings.Authentication.Schemes = AuthenticationSchemes.None; + options.ListenerSettings.Authentication.AllowAnonymous = true; }); } @@ -52,8 +52,8 @@ namespace SelfHostServer .UseStartup() .UseWebListener(options => { - options.Listener.AuthenticationManager.AuthenticationSchemes = AuthenticationSchemes.None; - options.Listener.AuthenticationManager.AllowAnonymous = true; + options.ListenerSettings.Authentication.Schemes = AuthenticationSchemes.None; + options.ListenerSettings.Authentication.AllowAnonymous = true; }) .Build(); diff --git a/src/Microsoft.AspNetCore.Server.WebListener/Internal/WebListenerOptionsSetup.cs b/src/Microsoft.AspNetCore.Server.WebListener/Internal/WebListenerOptionsSetup.cs index eeba2f8b91..6b87fd31ad 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/Internal/WebListenerOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/Internal/WebListenerOptionsSetup.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. +// Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Server.WebListener.Internal public void Configure(WebListenerOptions options) { - options.Listener = new Microsoft.Net.Http.Server.WebListener(_loggerFactory); + options.ListenerSettings.Logger = _loggerFactory.CreateLogger(); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs b/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs index 591b060900..3de914ea0d 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/MessagePump.cs @@ -57,15 +57,16 @@ namespace Microsoft.AspNetCore.Server.WebListener throw new ArgumentNullException(nameof(loggerFactory)); } - _listener = options.Value?.Listener ?? new Microsoft.Net.Http.Server.WebListener(loggerFactory); + var optionsInstance = options.Value; + _listener = new Microsoft.Net.Http.Server.WebListener(optionsInstance.ListenerSettings); _logger = LogHelper.CreateLogger(loggerFactory, typeof(MessagePump)); Features = new FeatureCollection(); _serverAddresses = new ServerAddressesFeature(); Features.Set(_serverAddresses); _processRequest = new Action(ProcessRequestAsync); - _maxAccepts = options.Value?.MaxAccepts ?? WebListenerOptions.DefaultMaxAccepts; - EnableResponseCaching = options.Value?.EnableResponseCaching ?? true; + _maxAccepts = optionsInstance.MaxAccepts; + EnableResponseCaching = optionsInstance.EnableResponseCaching; _shutdownSignal = new ManualResetEvent(false); } @@ -94,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.WebListener _application = new ApplicationWrapper(application); - if (_listener.UrlPrefixes.Count == 0) + if (_listener.Settings.UrlPrefixes.Count == 0) { throw new InvalidOperationException("No address prefixes were defined."); } @@ -224,7 +225,7 @@ namespace Microsoft.AspNetCore.Server.WebListener { foreach (var value in addresses) { - listener.UrlPrefixes.Add(UrlPrefix.Create(value)); + listener.Settings.UrlPrefixes.Add(UrlPrefix.Create(value)); } } diff --git a/src/Microsoft.AspNetCore.Server.WebListener/WebListenerOptions.cs b/src/Microsoft.AspNetCore.Server.WebListener/WebListenerOptions.cs index 9613f6d468..aa4bbbc0f5 100644 --- a/src/Microsoft.AspNetCore.Server.WebListener/WebListenerOptions.cs +++ b/src/Microsoft.AspNetCore.Server.WebListener/WebListenerOptions.cs @@ -16,6 +16,7 @@ // permissions and limitations under the License. using System; +using Microsoft.Net.Http.Server; namespace Microsoft.AspNetCore.Server.WebListener { @@ -23,10 +24,21 @@ namespace Microsoft.AspNetCore.Server.WebListener { internal static readonly int DefaultMaxAccepts = 5 * Environment.ProcessorCount; - public Microsoft.Net.Http.Server.WebListener Listener { get; set; } = new Microsoft.Net.Http.Server.WebListener(); + /// + /// Settings for the underlying WebListener instance. + /// + public WebListenerSettings ListenerSettings { get; } = new WebListenerSettings(); + /// + /// The maximum number of concurrent calls to WebListener.AcceptAsync(). + /// public int MaxAccepts { get; set; } = DefaultMaxAccepts; + /// + /// Attempts kernel mode caching for responses with eligible headers. The response may not include + /// Set-Cookie, Vary, or Pragma headers. It must include a Cache-Control header with Public and + /// either a Shared-Max-Age or Max-Age value, or an Expires header. + /// public bool EnableResponseCaching { get; set; } = true; } } diff --git a/src/Microsoft.Net.Http.Server/AuthenticationManager.cs b/src/Microsoft.Net.Http.Server/AuthenticationManager.cs index c1f0ed0ac1..fb8766996a 100644 --- a/src/Microsoft.Net.Http.Server/AuthenticationManager.cs +++ b/src/Microsoft.Net.Http.Server/AuthenticationManager.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Security.Claims; @@ -42,24 +43,21 @@ namespace Microsoft.Net.Http.Server private static readonly int AuthInfoSize = Marshal.SizeOf(); - private WebListener _server; + private UrlGroup _urlGroup; private AuthenticationSchemes _authSchemes; private bool _allowAnonymous = true; - internal AuthenticationManager(WebListener listener) + internal AuthenticationManager() { - _server = listener; } - #region Properties - - public AuthenticationSchemes AuthenticationSchemes + public AuthenticationSchemes Schemes { get { return _authSchemes; } set { _authSchemes = value; - SetServerSecurity(); + SetUrlGroupSecurity(); } } @@ -69,10 +67,21 @@ namespace Microsoft.Net.Http.Server set { _allowAnonymous = value; } } - #endregion Properties - - private unsafe void SetServerSecurity() + internal void SetUrlGroupSecurity(UrlGroup urlGroup) { + Debug.Assert(_urlGroup == null, "SetUrlGroupSecurity called more than once."); + _urlGroup = urlGroup; + SetUrlGroupSecurity(); + } + + private unsafe void SetUrlGroupSecurity() + { + if (_urlGroup == null) + { + // Not started yet. + return; + } + HttpApi.HTTP_SERVER_AUTHENTICATION_INFO authInfo = new HttpApi.HTTP_SERVER_AUTHENTICATION_INFO(); @@ -91,7 +100,7 @@ namespace Microsoft.Net.Http.Server IntPtr infoptr = new IntPtr(&authInfo); - _server.UrlGroup.SetProperty( + _urlGroup.SetProperty( HttpApi.HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty, infoptr, (uint)AuthInfoSize); } diff --git a/src/Microsoft.Net.Http.Server/LogHelper.cs b/src/Microsoft.Net.Http.Server/LogHelper.cs index 4d4816462e..6ec1c67a7b 100644 --- a/src/Microsoft.Net.Http.Server/LogHelper.cs +++ b/src/Microsoft.Net.Http.Server/LogHelper.cs @@ -29,16 +29,6 @@ namespace Microsoft.Net.Http.Server { internal static class LogHelper { - internal static ILogger CreateLogger(ILoggerFactory factory, Type type) - { - if (factory == null) - { - return new NullLogger(); - } - - return factory.CreateLogger(type.FullName); - } - internal static void LogInfo(ILogger logger, string data) { if (logger == null) @@ -86,29 +76,5 @@ namespace Microsoft.Net.Http.Server logger.LogError(location + "; " + message); } } - - private class NullLogger : ILogger - { - public IDisposable BeginScope(TState state) - { - return new NullDispose(); - } - - public bool IsEnabled(LogLevel logLevel) - { - return false; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - } - - private class NullDispose : IDisposable - { - public void Dispose() - { - } - } - } } } diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs b/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs index 87b07e5996..c650041703 100644 --- a/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs +++ b/src/Microsoft.Net.Http.Server/NativeInterop/RequestQueue.cs @@ -16,11 +16,8 @@ // 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 @@ -32,6 +29,7 @@ namespace Microsoft.Net.Http.Server private readonly UrlGroup _urlGroup; private readonly ILogger _logger; + private bool _disposed; internal RequestQueue(UrlGroup urlGroup, ILogger logger) { @@ -66,6 +64,7 @@ namespace Microsoft.Net.Http.Server internal unsafe void AttachToUrlGroup() { + CheckDisposed(); // Set the association between request queue and url group. After this, requests for registered urls will // get delivered to this request queue. @@ -81,6 +80,7 @@ namespace Microsoft.Net.Http.Server internal unsafe void DetachFromUrlGroup() { + CheckDisposed(); // 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 @@ -100,6 +100,8 @@ namespace Microsoft.Net.Http.Server // The listener must be active for this to work. internal unsafe void SetLengthLimit(long length) { + CheckDisposed(); + var result = HttpApi.HttpSetRequestQueueProperty(Handle, HttpApi.HTTP_SERVER_PROPERTY.HttpServerQueueLengthProperty, new IntPtr((void*)&length), (uint)Marshal.SizeOf(), 0, IntPtr.Zero); @@ -112,8 +114,22 @@ namespace Microsoft.Net.Http.Server public void Dispose() { + if (_disposed) + { + return; + } + + _disposed = true; BoundHandle.Dispose(); Handle.Dispose(); } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().FullName); + } + } } } diff --git a/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs b/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs index 385c7c61fa..23a5452122 100644 --- a/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs +++ b/src/Microsoft.Net.Http.Server/NativeInterop/UrlGroup.cs @@ -50,7 +50,8 @@ namespace Microsoft.Net.Http.Server internal void SetProperty(HttpApi.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true) { Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer"); - + CheckDisposed(); + var statusCode = HttpApi.HttpSetUrlGroupProperty(Id, property, info, infosize); if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) @@ -67,6 +68,7 @@ namespace Microsoft.Net.Http.Server internal void RegisterPrefix(string uriPrefix, int contextId) { LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix); + CheckDisposed(); var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0); @@ -86,6 +88,7 @@ namespace Microsoft.Net.Http.Server internal bool UnregisterPrefix(string uriPrefix) { LogHelper.LogInfo(_logger, "Stop listening on prefix: " + uriPrefix); + CheckDisposed(); var statusCode = HttpApi.HttpRemoveUrlFromUrlGroup(Id, uriPrefix, 0); @@ -115,5 +118,13 @@ namespace Microsoft.Net.Http.Server } Id = 0; } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().FullName); + } + } } } diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs index 481f600692..6321c28b1d 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Request.cs @@ -85,7 +85,7 @@ namespace Microsoft.Net.Http.Server QueryString = Marshal.PtrToStringUni((IntPtr)cookedUrl.pQueryString, cookedUrl.QueryStringLength / 2); } - var prefix = requestContext.Server.UrlPrefixes.GetPrefix((int)memoryBlob.RequestBlob->UrlContext); + var prefix = requestContext.Server.Settings.UrlPrefixes.GetPrefix((int)memoryBlob.RequestBlob->UrlContext); var originalPath = RequestUriBuilder.GetRequestPath(RawUrl, cookedUrlPath, RequestContext.Logger); // These paths are both unescaped already. diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs index 023d4edab8..3ff9af71cd 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/Response.cs @@ -64,7 +64,7 @@ namespace Microsoft.Net.Http.Server _expectedBodyLength = 0; _nativeStream = null; _cacheTtl = null; - _authChallenges = RequestContext.Server.AuthenticationManager.AuthenticationSchemes; + _authChallenges = RequestContext.Server.Settings.Authentication.Schemes; } private enum ResponseState @@ -412,7 +412,7 @@ namespace Microsoft.Net.Http.Server // 401 if (StatusCode == (ushort)HttpStatusCode.Unauthorized) { - RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext); + RequestContext.Server.Settings.Authentication.SetAuthenticationChallenge(RequestContext); } var flags = HttpApi.HTTP_FLAGS.NONE; diff --git a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs index 04e8f16e64..be0d5ad1a8 100644 --- a/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.Net.Http.Server/RequestProcessing/ResponseStream.cs @@ -171,7 +171,7 @@ namespace Microsoft.Net.Http.Server IntPtr.Zero); } - if (_requestContext.Server.IgnoreWriteExceptions) + if (_requestContext.Server.Settings.IgnoreWriteExceptions) { statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; } @@ -338,7 +338,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING) { asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && started) + if (_requestContext.Server.Settings.IgnoreWriteExceptions && started) { asyncResult.Complete(); } @@ -602,7 +602,7 @@ namespace Microsoft.Net.Http.Server if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { asyncResult.Dispose(); - if (_requestContext.Server.IgnoreWriteExceptions && started) + if (_requestContext.Server.Settings.IgnoreWriteExceptions && started) { asyncResult.Complete(); } diff --git a/src/Microsoft.Net.Http.Server/TimeoutManager.cs b/src/Microsoft.Net.Http.Server/TimeoutManager.cs index 8c7155f710..9762c254a7 100644 --- a/src/Microsoft.Net.Http.Server/TimeoutManager.cs +++ b/src/Microsoft.Net.Http.Server/TimeoutManager.cs @@ -37,14 +37,12 @@ namespace Microsoft.Net.Http.Server private static readonly int TimeoutLimitSize = Marshal.SizeOf(); - private WebListener _server; + private UrlGroup _urlGroup; private int[] _timeouts; private uint _minSendBytesPerSecond; - internal TimeoutManager(WebListener listener) + internal TimeoutManager() { - _server = listener; - // We have to maintain local state since we allow applications to set individual timeouts. Native Http // API for setting timeouts expects all timeout values in every call so we have remember timeout values // to fill in the blanks. Except MinSendBytesPerSecond, local state for remaining five timeouts is @@ -180,7 +178,7 @@ namespace Microsoft.Net.Http.Server throw new ArgumentOutOfRangeException("value"); } - SetServerTimeouts(_timeouts, (uint)value); + SetUrlGroupTimeouts(_timeouts, (uint)value); _minSendBytesPerSecond = (uint)value; } } @@ -211,12 +209,24 @@ namespace Microsoft.Net.Http.Server // call succeeds, update local state. var newTimeouts = (int[])_timeouts.Clone(); newTimeouts[(int)type] = (int)timeoutValue; - SetServerTimeouts(newTimeouts, _minSendBytesPerSecond); + SetUrlGroupTimeouts(newTimeouts, _minSendBytesPerSecond); _timeouts[(int)type] = (int)timeoutValue; } - private unsafe void SetServerTimeouts(int[] timeouts, uint minSendBytesPerSecond) + internal void SetUrlGroupTimeouts(UrlGroup urlGroup) { + _urlGroup = urlGroup; + SetUrlGroupTimeouts(_timeouts, _minSendBytesPerSecond); + } + + private unsafe void SetUrlGroupTimeouts(int[] timeouts, uint minSendBytesPerSecond) + { + if (_urlGroup == null) + { + // Not started yet + return; + } + var timeoutinfo = new HttpApi.HTTP_TIMEOUT_LIMIT_INFO(); timeoutinfo.Flags = HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; @@ -234,7 +244,7 @@ namespace Microsoft.Net.Http.Server var infoptr = new IntPtr(&timeoutinfo); - _server.UrlGroup.SetProperty( + _urlGroup.SetProperty( HttpApi.HTTP_SERVER_PROPERTY.HttpServerTimeoutsProperty, infoptr, (uint)TimeoutLimitSize); } diff --git a/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs b/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs index b8f417ca14..bfef83e0d2 100644 --- a/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs +++ b/src/Microsoft.Net.Http.Server/UrlPrefixCollection.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Collections; using System.Collections.Generic; @@ -12,13 +11,12 @@ namespace Microsoft.Net.Http.Server /// public class UrlPrefixCollection : ICollection { - private readonly WebListener _webListener; private readonly IDictionary _prefixes = new Dictionary(1); + private UrlGroup _urlGroup; private int _nextId = 1; - internal UrlPrefixCollection(WebListener webListener) + internal UrlPrefixCollection() { - _webListener = webListener; } public int Count @@ -47,9 +45,9 @@ namespace Microsoft.Net.Http.Server lock (_prefixes) { var id = _nextId++; - if (_webListener.IsListening) + if (_urlGroup != null) { - _webListener.UrlGroup.RegisterPrefix(item.FullPrefix, id); + _urlGroup.RegisterPrefix(item.FullPrefix, id); } _prefixes.Add(id, item); } @@ -67,7 +65,7 @@ namespace Microsoft.Net.Http.Server { lock (_prefixes) { - if (_webListener.IsListening) + if (_urlGroup != null) { UnregisterAllPrefixes(); } @@ -106,9 +104,9 @@ namespace Microsoft.Net.Http.Server if (pair.Value.Equals(item)) { id = pair.Key; - if (_webListener.IsListening) + if (_urlGroup != null) { - _webListener.UrlGroup.UnregisterPrefix(pair.Value.FullPrefix); + _urlGroup.UnregisterPrefix(pair.Value.FullPrefix); } } } @@ -134,15 +132,16 @@ namespace Microsoft.Net.Http.Server return GetEnumerator(); } - internal void RegisterAllPrefixes() + internal void RegisterAllPrefixes(UrlGroup urlGroup) { lock (_prefixes) { + _urlGroup = urlGroup; // go through the uri list and register for each one of them 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. - _webListener.UrlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key); + _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key); } } } @@ -155,7 +154,7 @@ namespace Microsoft.Net.Http.Server foreach (var prefix in _prefixes.Values) { // ignore possible failures - _webListener.UrlGroup.UnregisterPrefix(prefix.FullPrefix); + _urlGroup.UnregisterPrefix(prefix.FullPrefix); } } } diff --git a/src/Microsoft.Net.Http.Server/WebListener.cs b/src/Microsoft.Net.Http.Server/WebListener.cs index 5f92de5fae..f7dde384fc 100644 --- a/src/Microsoft.Net.Http.Server/WebListener.cs +++ b/src/Microsoft.Net.Http.Server/WebListener.cs @@ -24,7 +24,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -36,8 +35,6 @@ namespace Microsoft.Net.Http.Server /// public sealed class WebListener : IDisposable { - private const long DefaultRequestQueueLength = 1000; // Http.sys default. - // 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 @@ -53,49 +50,39 @@ namespace Microsoft.Net.Http.Server // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; - private ILogger _logger; - private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. - private bool _ignoreWriteExceptions; private ServerSession _serverSession; private UrlGroup _urlGroup; private RequestQueue _requestQueue; - private TimeoutManager _timeoutManager; - private AuthenticationManager _authManager; private DisconnectListener _disconnectListener; private object _internalLock; - private UrlPrefixCollection _urlPrefixes; - - // The native request queue - private long? _requestQueueLength; - public WebListener() - : this(null) + : this(new WebListenerSettings()) { } - public WebListener(ILoggerFactory factory) + public WebListener(WebListenerSettings settings) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + if (!HttpApi.Supported) { throw new PlatformNotSupportedException(); } - _logger = LogHelper.CreateLogger(factory, typeof(WebListener)); + Debug.Assert(HttpApi.ApiVersion == HttpApi.HTTP_API_VERSION.Version20, "Invalid Http api version"); - Debug.Assert(HttpApi.ApiVersion == - HttpApi.HTTP_API_VERSION.Version20, "Invalid Http api version"); + Settings = settings; _state = State.Stopped; _internalLock = new object(); - _urlPrefixes = new UrlPrefixCollection(this); - _timeoutManager = new TimeoutManager(this); - _authManager = new AuthenticationManager(this); - // V2 initialization sequence: // 1. Create server session // 2. Create url group @@ -107,11 +94,11 @@ namespace Microsoft.Net.Http.Server { _serverSession = new ServerSession(); - _urlGroup = new UrlGroup(_serverSession, _logger); + _urlGroup = new UrlGroup(_serverSession, Logger); - _requestQueue = new RequestQueue(_urlGroup, _logger); + _requestQueue = new RequestQueue(_urlGroup, Logger); - _disconnectListener = new DisconnectListener(_requestQueue, _logger); + _disconnectListener = new DisconnectListener(_requestQueue, Logger); } catch (Exception exception) { @@ -119,7 +106,7 @@ namespace Microsoft.Net.Http.Server _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); - LogHelper.LogException(_logger, ".Ctor", exception); + LogHelper.LogException(Logger, ".Ctor", exception); throw; } } @@ -133,7 +120,7 @@ namespace Microsoft.Net.Http.Server internal ILogger Logger { - get { return _logger; } + get { return Settings.Logger; } } internal UrlGroup UrlGroup @@ -151,66 +138,13 @@ namespace Microsoft.Net.Http.Server get { return _disconnectListener; } } - // TODO: https://github.com/aspnet/WebListener/issues/173 - internal bool IgnoreWriteExceptions - { - get { return _ignoreWriteExceptions; } - set - { - CheckDisposed(); - _ignoreWriteExceptions = value; - } - } - - public UrlPrefixCollection UrlPrefixes - { - get { return _urlPrefixes; } - } - - /// - /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. - /// - public TimeoutManager TimeoutManager - { - get { return _timeoutManager; } - } - - /// - /// Http.Sys authentication settings. - /// - public AuthenticationManager AuthenticationManager - { - get { return _authManager; } - } + public WebListenerSettings Settings { get; } public bool IsListening { get { return _state == State.Started; } } - /// - /// Sets the maximum number of requests that will be queued up in Http.Sys. - /// - /// - public void SetRequestQueueLimit(long limit) - { - CheckDisposed(); - if (limit <= 0) - { - throw new ArgumentOutOfRangeException("limit", limit, string.Empty); - } - - // Don't try to change it if the new limit is the same - if ((!_requestQueueLength.HasValue && limit == DefaultRequestQueueLength) - || (_requestQueueLength.HasValue && limit == _requestQueueLength.Value)) - { - return; - } - - _requestQueueLength = limit; - _requestQueue.SetLengthLimit(_requestQueueLength.Value); - } - /// /// Start accepting incoming requests. /// @@ -218,7 +152,7 @@ namespace Microsoft.Net.Http.Server { CheckDisposed(); - LogHelper.LogInfo(_logger, "Start"); + LogHelper.LogInfo(Logger, "Start"); // 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 @@ -233,12 +167,16 @@ namespace Microsoft.Net.Http.Server return; } + Settings.Authentication.SetUrlGroupSecurity(UrlGroup); + Settings.Timeouts.SetUrlGroupTimeouts(UrlGroup); + Settings.SetRequestQueueLimit(RequestQueue); + _requestQueue.AttachToUrlGroup(); // All resources are set up correctly. Now add all prefixes. try { - _urlPrefixes.RegisterAllPrefixes(); + Settings.UrlPrefixes.RegisterAllPrefixes(UrlGroup); } catch (WebListenerException) { @@ -254,7 +192,7 @@ namespace Microsoft.Net.Http.Server // Make sure the HttpListener instance can't be used if Start() failed. _state = State.Disposed; DisposeInternal(); - LogHelper.LogException(_logger, "Start", exception); + LogHelper.LogException(Logger, "Start", exception); throw; } } @@ -272,7 +210,7 @@ namespace Microsoft.Net.Http.Server return; } - _urlPrefixes.UnregisterAllPrefixes(); + Settings.UrlPrefixes.UnregisterAllPrefixes(); _state = State.Stopped; @@ -281,7 +219,7 @@ namespace Microsoft.Net.Http.Server } catch (Exception exception) { - LogHelper.LogException(_logger, "Stop", exception); + LogHelper.LogException(Logger, "Stop", exception); throw; } } @@ -309,14 +247,14 @@ namespace Microsoft.Net.Http.Server { return; } - LogHelper.LogInfo(_logger, "Dispose"); + LogHelper.LogInfo(Logger, "Dispose"); Stop(); DisposeInternal(); } catch (Exception exception) { - LogHelper.LogException(_logger, "Dispose", exception); + LogHelper.LogException(Logger, "Dispose", exception); throw; } finally @@ -371,7 +309,7 @@ namespace Microsoft.Net.Http.Server } catch (Exception exception) { - LogHelper.LogException(_logger, "GetContextAsync", exception); + LogHelper.LogException(Logger, "GetContextAsync", exception); throw; } @@ -392,10 +330,10 @@ namespace Microsoft.Net.Http.Server internal unsafe bool ValidateAuth(NativeRequestContext requestMemory) { var requestV2 = (HttpApi.HTTP_REQUEST_V2*)requestMemory.RequestBlob; - if (!AuthenticationManager.AllowAnonymous && !AuthenticationManager.CheckAuthenticated(requestV2->pRequestInfo)) + if (!Settings.Authentication.AllowAnonymous && !AuthenticationManager.CheckAuthenticated(requestV2->pRequestInfo)) { SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.Unauthorized, - AuthenticationManager.GenerateChallenges(AuthenticationManager.AuthenticationSchemes)); + AuthenticationManager.GenerateChallenges(Settings.Authentication.Schemes)); return false; } return true; @@ -509,7 +447,7 @@ namespace Microsoft.Net.Http.Server } } - internal void CheckDisposed() + private void CheckDisposed() { if (_state == State.Disposed) { diff --git a/src/Microsoft.Net.Http.Server/WebListenerSettings.cs b/src/Microsoft.Net.Http.Server/WebListenerSettings.cs new file mode 100644 index 0000000000..dd65bc5c3f --- /dev/null +++ b/src/Microsoft.Net.Http.Server/WebListenerSettings.cs @@ -0,0 +1,114 @@ +// 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions.Internal; + +namespace Microsoft.Net.Http.Server +{ + public class WebListenerSettings + { + private const long DefaultRequestQueueLength = 1000; // Http.sys default. + + // The native request queue + private long _requestQueueLength = DefaultRequestQueueLength; + private RequestQueue _requestQueue; + private ILogger _logger = NullLogger.Instance; + + public WebListenerSettings() + { + } + + /// + /// The logger that will be used to create the WebListener instance. This should not be changed + /// after creating the listener. + /// + public ILogger Logger + { + get { return _logger; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + _logger = value; + } + } + + /// + /// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing + /// the listener. + /// + public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection(); + + /// + /// Http.Sys authentication settings. These may be modified at any time prior to disposing + /// the listener. + /// + public AuthenticationManager Authentication { get; } = new AuthenticationManager(); + + /// + /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry. + /// These may be modified at any time prior to disposing the listener. + /// + public TimeoutManager Timeouts { get; } = new TimeoutManager(); + + + // TODO: https://github.com/aspnet/WebListener/issues/173 + /// + /// Gets or Sets if response body writes that fail due to client disconnects should throw exceptions or + /// complete normally. The default is true. + /// + internal bool IgnoreWriteExceptions { get; set; } = true; + + /// + /// Gets or sets the maximum number of requests that will be queued up in Http.Sys. + /// + public long RequestQueueLimit + { + get + { + return _requestQueueLength; + } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, string.Empty); + } + + if (_requestQueue != null) + { + _requestQueue.SetLengthLimit(_requestQueueLength); + } + // Only store it if it succeeds or hasn't started yet + _requestQueueLength = value; + } + } + + internal void SetRequestQueueLimit(RequestQueue requestQueue) + { + _requestQueue = requestQueue; + if (_requestQueueLength != DefaultRequestQueueLength) + { + _requestQueue.SetLengthLimit(_requestQueueLength); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/RequestTests.cs index 08d075089d..62b1f18d4f 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/RequestTests.cs @@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Server.WebListener foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" }) { - server.Listener.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path)); + server.Listener.Settings.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path)); } server.Start(new DummyApplication(app)); diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ServerTests.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ServerTests.cs index a439324a81..6acf44068d 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ServerTests.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/ServerTests.cs @@ -256,8 +256,8 @@ namespace Microsoft.AspNetCore.Server.WebListener using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { } var server = new MessagePump(Options.Create(new WebListenerOptions()), new LoggerFactory()); - server.Listener.UrlPrefixes.Add(UrlPrefix.Create(address)); - server.Listener.SetRequestQueueLimit(1001); + server.Listener.Settings.UrlPrefixes.Add(UrlPrefix.Create(address)); + server.Listener.Settings.RequestQueueLimit = 1001; using (server) { diff --git a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/Utilities.cs b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/Utilities.cs index 35dbaa848b..02a933d838 100644 --- a/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/Utilities.cs +++ b/test/Microsoft.AspNetCore.Server.WebListener.FunctionalTests/Utilities.cs @@ -67,8 +67,8 @@ namespace Microsoft.AspNetCore.Server.WebListener var server = new MessagePump(Options.Create(new WebListenerOptions()), new LoggerFactory()); server.Features.Get().Addresses.Add(baseAddress); - server.Listener.AuthenticationManager.AuthenticationSchemes = authType; - server.Listener.AuthenticationManager.AllowAnonymous = allowAnonymous; + server.Listener.Settings.Authentication.Schemes = authType; + server.Listener.Settings.Authentication.AllowAnonymous = allowAnonymous; try { server.Start(new DummyApplication(app)); diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/RequestTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/RequestTests.cs index d69ac9ee81..31e5e850bf 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/RequestTests.cs @@ -136,7 +136,7 @@ namespace Microsoft.Net.Http.Server var uriBuilder = new UriBuilder(root); foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" }) { - server.UrlPrefixes.Add(UrlPrefix.Create(uriBuilder.Scheme, uriBuilder.Host, uriBuilder.Port, path)); + server.Settings.UrlPrefixes.Add(UrlPrefix.Create(uriBuilder.Scheme, uriBuilder.Host, uriBuilder.Port, path)); } server.Start(); diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/ServerTests.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/ServerTests.cs index 6b444e4ba6..332774d211 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/ServerTests.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/ServerTests.cs @@ -171,7 +171,7 @@ namespace Microsoft.Net.Http.Server string address; using (var server = Utilities.CreateHttpServer(out address)) { - server.SetRequestQueueLimit(1001); + server.Settings.RequestQueueLimit = 1001; var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); @@ -199,7 +199,7 @@ namespace Microsoft.Net.Http.Server Assert.Equal(string.Empty, response); address += "pathbase/"; - server.UrlPrefixes.Add(address); + server.Settings.UrlPrefixes.Add(address); responseTask = SendRequestAsync(address); @@ -220,7 +220,7 @@ namespace Microsoft.Net.Http.Server using (var server = Utilities.CreateHttpServer(out address)) { address += "pathbase/"; - server.UrlPrefixes.Add(address); + server.Settings.UrlPrefixes.Add(address); var responseTask = SendRequestAsync(address); var context = await server.AcceptAsync(); @@ -231,7 +231,7 @@ namespace Microsoft.Net.Http.Server var response = await responseTask; Assert.Equal(string.Empty, response); - Assert.True(server.UrlPrefixes.Remove(address)); + Assert.True(server.Settings.UrlPrefixes.Remove(address)); responseTask = SendRequestAsync(address); diff --git a/test/Microsoft.Net.Http.Server.FunctionalTests/Utilities.cs b/test/Microsoft.Net.Http.Server.FunctionalTests/Utilities.cs index 73dc5466b7..3ba29a660a 100644 --- a/test/Microsoft.Net.Http.Server.FunctionalTests/Utilities.cs +++ b/test/Microsoft.Net.Http.Server.FunctionalTests/Utilities.cs @@ -17,8 +17,8 @@ namespace Microsoft.Net.Http.Server internal static WebListener CreateHttpAuthServer(AuthenticationSchemes authScheme, bool allowAnonymos, out string baseAddress) { var listener = CreateHttpServer(out baseAddress); - listener.AuthenticationManager.AuthenticationSchemes = authScheme; - listener.AuthenticationManager.AllowAnonymous = allowAnonymos; + listener.Settings.Authentication.Schemes = authScheme; + listener.Settings.Authentication.AllowAnonymous = allowAnonymos; return listener; } @@ -45,7 +45,7 @@ namespace Microsoft.Net.Http.Server root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port; baseAddress = prefix.ToString(); var listener = new WebListener(); - listener.UrlPrefixes.Add(prefix); + listener.Settings.UrlPrefixes.Add(prefix); try { listener.Start(); @@ -61,7 +61,6 @@ namespace Microsoft.Net.Http.Server throw new Exception("Failed to locate a free port."); } - internal static WebListener CreateHttpsServer() { return CreateServer("https", "localhost", 9090, string.Empty); @@ -70,7 +69,7 @@ namespace Microsoft.Net.Http.Server internal static WebListener CreateServer(string scheme, string host, int port, string path) { WebListener listener = new WebListener(); - listener.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path)); + listener.Settings.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path)); listener.Start(); return listener; }