diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs index 8236a388a564..9d7f3a1ad187 100644 --- a/src/Http/Http/src/HttpContextFactory.cs +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http @@ -35,7 +36,7 @@ public HttpContext Create(IFeatureCollection featureCollection) throw new ArgumentNullException(nameof(featureCollection)); } - var httpContext = new DefaultHttpContext(featureCollection); + var httpContext = CreateHttpContext(featureCollection); if (_httpContextAccessor != null) { _httpContextAccessor.HttpContext = httpContext; @@ -47,6 +48,16 @@ public HttpContext Create(IFeatureCollection featureCollection) return httpContext; } + private static HttpContext CreateHttpContext(IFeatureCollection featureCollection) + { + if (featureCollection is IHttpContextContainer container) + { + return container.HttpContext; + } + + return new ReusableHttpContext(featureCollection); + } + public void Dispose(HttpContext httpContext) { if (_httpContextAccessor != null) @@ -55,4 +66,4 @@ public void Dispose(HttpContext httpContext) } } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/IHttpContextContainer.cs b/src/Http/Http/src/IHttpContextContainer.cs new file mode 100644 index 000000000000..f77b36ec1de9 --- /dev/null +++ b/src/Http/Http/src/IHttpContextContainer.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Http +{ + public interface IHttpContextContainer + { + HttpContext HttpContext { get; } + } +} diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs index e2512f60dc1b..cf8ac92a3b5f 100644 --- a/src/Http/Http/src/Internal/DefaultHttpRequest.cs +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -171,4 +171,4 @@ struct FeatureInterfaces public IRouteValuesFeature RouteValues; } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs index 3ca05035f5c0..6a812426d850 100644 --- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -136,4 +136,4 @@ struct FeatureInterfaces public IResponseCookiesFeature Cookies; } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/Internal/ReusableConnectionInfo.cs b/src/Http/Http/src/Internal/ReusableConnectionInfo.cs new file mode 100644 index 000000000000..9891ee6cbc80 --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableConnectionInfo.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableConnectionInfo : ConnectionInfo + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newHttpConnectionFeature = f => new HttpConnectionFeature(); + private readonly static Func _newTlsConnectionFeature = f => new TlsConnectionFeature(); + + private FeatureReferences _features; + + public ReusableConnectionInfo(IFeatureCollection features) + { + Initialize(features); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public void Uninitialize() + { + _features = default(FeatureReferences); + } + + private IHttpConnectionFeature HttpConnectionFeature => + _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature); + + private ITlsConnectionFeature TlsConnectionFeature => + _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature); + + /// + public override string Id + { + get { return HttpConnectionFeature.ConnectionId; } + set { HttpConnectionFeature.ConnectionId = value; } + } + + public override IPAddress RemoteIpAddress + { + get { return HttpConnectionFeature.RemoteIpAddress; } + set { HttpConnectionFeature.RemoteIpAddress = value; } + } + + public override int RemotePort + { + get { return HttpConnectionFeature.RemotePort; } + set { HttpConnectionFeature.RemotePort = value; } + } + + public override IPAddress LocalIpAddress + { + get { return HttpConnectionFeature.LocalIpAddress; } + set { HttpConnectionFeature.LocalIpAddress = value; } + } + + public override int LocalPort + { + get { return HttpConnectionFeature.LocalPort; } + set { HttpConnectionFeature.LocalPort = value; } + } + + public override X509Certificate2 ClientCertificate + { + get { return TlsConnectionFeature.ClientCertificate; } + set { TlsConnectionFeature.ClientCertificate = value; } + } + + public override Task GetClientCertificateAsync(CancellationToken cancellationToken = default) + { + return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpConnectionFeature Connection; + public ITlsConnectionFeature TlsConnection; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpContext.cs b/src/Http/Http/src/Internal/ReusableHttpContext.cs new file mode 100644 index 000000000000..29a7d7f9077c --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpContext.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Threading; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpContext : HttpContext + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newItemsFeature = f => new ItemsFeature(); + private readonly static Func _newServiceProvidersFeature = f => new ServiceProvidersFeature(); + private readonly static Func _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); + private readonly static Func _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); + private readonly static Func _newSessionFeature = f => new DefaultSessionFeature(); + private readonly static Func _nullSessionFeature = f => null; + private readonly static Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + + private FeatureReferences _features; + + private ReusableHttpRequest _request; + private ReusableHttpResponse _response; + + private ReusableConnectionInfo _connection; + private ReusableWebSocketManager _websockets; + + public ReusableHttpContext(IFeatureCollection features) + { + _features = new FeatureReferences(features); + _request = new ReusableHttpRequest(this); + _response = new ReusableHttpResponse(this); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + _request.Initialize(this); + _response.Initialize(this); + _connection?.Initialize(features); + _websockets?.Initialize(features); + } + + public void Uninitialize() + { + _features = default; + + _request.Uninitialize(); + _response.Uninitialize(); + _connection?.Uninitialize(); + _websockets?.Uninitialize(); + } + + private IItemsFeature ItemsFeature => + _features.Fetch(ref _features.Cache.Items, _newItemsFeature); + + private IServiceProvidersFeature ServiceProvidersFeature => + _features.Fetch(ref _features.Cache.ServiceProviders, _newServiceProvidersFeature); + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature); + + private IHttpRequestLifetimeFeature LifetimeFeature => + _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature); + + private ISessionFeature SessionFeature => + _features.Fetch(ref _features.Cache.Session, _newSessionFeature); + + private ISessionFeature SessionFeatureOrNull => + _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); + + + private IHttpRequestIdentifierFeature RequestIdentifierFeature => + _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature); + + public override IFeatureCollection Features => _features.Collection; + + public override HttpRequest Request => _request; + + public override HttpResponse Response => _response; + + public override ConnectionInfo Connection => _connection ?? (_connection = new ReusableConnectionInfo(_features.Collection)); + + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public override AuthenticationManager Authentication => throw new NotSupportedException(); + + public override WebSocketManager WebSockets => _websockets ?? (_websockets = new ReusableWebSocketManager(_features.Collection)); + + + public override ClaimsPrincipal User + { + get + { + var user = HttpAuthenticationFeature.User; + if (user == null) + { + user = new ClaimsPrincipal(new ClaimsIdentity()); + HttpAuthenticationFeature.User = user; + } + return user; + } + set { HttpAuthenticationFeature.User = value; } + } + + public override IDictionary Items + { + get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } + } + + public override IServiceProvider RequestServices + { + get { return ServiceProvidersFeature.RequestServices; } + set { ServiceProvidersFeature.RequestServices = value; } + } + + public override CancellationToken RequestAborted + { + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } + } + + public override string TraceIdentifier + { + get { return RequestIdentifierFeature.TraceIdentifier; } + set { RequestIdentifierFeature.TraceIdentifier = value; } + } + + public override ISession Session + { + get + { + var feature = SessionFeatureOrNull; + if (feature == null) + { + throw new InvalidOperationException("Session has not been configured for this application " + + "or request."); + } + return feature.Session; + } + set + { + SessionFeature.Session = value; + } + } + + public override void Abort() + { + LifetimeFeature.Abort(); + } + + struct FeatureInterfaces + { + public IItemsFeature Items; + public IServiceProvidersFeature ServiceProviders; + public IHttpAuthenticationFeature Authentication; + public IHttpRequestLifetimeFeature Lifetime; + public ISessionFeature Session; + public IHttpRequestIdentifierFeature RequestIdentifier; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpRequest.cs b/src/Http/Http/src/Internal/ReusableHttpRequest.cs new file mode 100644 index 000000000000..f491acd41c23 --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpRequest.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpRequest : HttpRequest + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _newQueryFeature = f => new QueryFeature(f); + private readonly static Func _newFormFeature = r => new FormFeature(r); + private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); + + private HttpContext _context; + private FeatureReferences _features; + + public ReusableHttpRequest(HttpContext context) + { + Initialize(context); + } + + public void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public void Uninitialize() + { + _context = null; + _features = default; + } + + public override HttpContext HttpContext => _context; + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IQueryFeature QueryFeature => + _features.Fetch(ref _features.Cache.Query, _newQueryFeature); + + private IFormFeature FormFeature => + _features.Fetch(ref _features.Cache.Form, this, _newFormFeature); + + private IRequestCookiesFeature RequestCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + + private IRouteValuesFeature RouteValuesFeature => + _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + + public override PathString PathBase + { + get { return new PathString(HttpRequestFeature.PathBase); } + set { HttpRequestFeature.PathBase = value.Value; } + } + + public override PathString Path + { + get { return new PathString(HttpRequestFeature.Path); } + set { HttpRequestFeature.Path = value.Value; } + } + + public override QueryString QueryString + { + get { return new QueryString(HttpRequestFeature.QueryString); } + set { HttpRequestFeature.QueryString = value.Value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override Stream Body + { + get { return HttpRequestFeature.Body; } + set { HttpRequestFeature.Body = value; } + } + + public override string Method + { + get { return HttpRequestFeature.Method; } + set { HttpRequestFeature.Method = value; } + } + + public override string Scheme + { + get { return HttpRequestFeature.Scheme; } + set { HttpRequestFeature.Scheme = value; } + } + + public override bool IsHttps + { + get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Constants.Https : Constants.Http; } + } + + public override HostString Host + { + get { return HostString.FromUriComponent(Headers["Host"]); } + set { Headers["Host"] = value.ToUriComponent(); } + } + + public override IQueryCollection Query + { + get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } + } + + public override string Protocol + { + get { return HttpRequestFeature.Protocol; } + set { HttpRequestFeature.Protocol = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpRequestFeature.Headers; } + } + + public override IRequestCookieCollection Cookies + { + get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } + } + + public override string ContentType + { + get { return Headers[HeaderNames.ContentType]; } + set { Headers[HeaderNames.ContentType] = value; } + } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } + + public override RouteValueDictionary RouteValues + { + get { return RouteValuesFeature.RouteValues; } + set { RouteValuesFeature.RouteValues = value; } + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IQueryFeature Query; + public IFormFeature Form; + public IRequestCookiesFeature Cookies; + public IRouteValuesFeature RouteValues; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpResponse.cs b/src/Http/Http/src/Internal/ReusableHttpResponse.cs new file mode 100644 index 000000000000..fd816351a82d --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpResponse.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpResponse : HttpResponse + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullResponseFeature = f => null; + private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences _features; + + public ReusableHttpResponse(HttpContext context) + { + Initialize(context); + } + + public void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public void Uninitialize() + { + _context = null; + _features = default; + } + + private IHttpResponseFeature HttpResponseFeature => + _features.Fetch(ref _features.Cache.Response, _nullResponseFeature); + + private IResponseCookiesFeature ResponseCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); + + + public override HttpContext HttpContext { get { return _context; } } + + public override int StatusCode + { + get { return HttpResponseFeature.StatusCode; } + set { HttpResponseFeature.StatusCode = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpResponseFeature.Headers; } + } + + public override Stream Body + { + get { return HttpResponseFeature.Body; } + set { HttpResponseFeature.Body = value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override string ContentType + { + get + { + return Headers[HeaderNames.ContentType]; + } + set + { + if (string.IsNullOrEmpty(value)) + { + HttpResponseFeature.Headers.Remove(HeaderNames.ContentType); + } + else + { + HttpResponseFeature.Headers[HeaderNames.ContentType] = value; + } + } + } + + public override IResponseCookies Cookies + { + get { return ResponseCookiesFeature.Cookies; } + } + + public override bool HasStarted + { + get { return HttpResponseFeature.HasStarted; } + } + + public override void OnStarting(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnStarting(callback, state); + } + + public override void OnCompleted(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnCompleted(callback, state); + } + + public override void Redirect(string location, bool permanent) + { + if (permanent) + { + HttpResponseFeature.StatusCode = 301; + } + else + { + HttpResponseFeature.StatusCode = 302; + } + + Headers[HeaderNames.Location] = location; + } + + struct FeatureInterfaces + { + public IHttpResponseFeature Response; + public IResponseCookiesFeature Cookies; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableWebSocketManager.cs b/src/Http/Http/src/Internal/ReusableWebSocketManager.cs new file mode 100644 index 000000000000..fcc144f188dd --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableWebSocketManager.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableWebSocketManager : WebSocketManager + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _nullWebSocketFeature = f => null; + + private FeatureReferences _features; + + public ReusableWebSocketManager(IFeatureCollection features) + { + Initialize(features); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public void Uninitialize() + { + _features = default; + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IHttpWebSocketFeature WebSocketFeature => + _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature); + + public override bool IsWebSocketRequest + { + get + { + return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; + } + } + + public override IList WebSocketRequestedProtocols + { + get + { + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); + } + } + + public override Task AcceptWebSocketAsync(string subProtocol) + { + if (WebSocketFeature == null) + { + throw new NotSupportedException("WebSockets are not supported"); + } + return WebSocketFeature.AcceptAsync(new WebSocketAcceptContext() { SubProtocol = subProtocol }); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IHttpWebSocketFeature WebSockets; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 773e5cc112b2..a9ee694a7dc3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -17,13 +17,14 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - public abstract partial class HttpProtocol : IHttpResponseControl + public abstract partial class HttpProtocol : IHttpContextContainer, IHttpResponseControl { private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close"); private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive"); @@ -63,6 +64,7 @@ public abstract partial class HttpProtocol : IHttpResponseControl private long _responseBytesWritten; private readonly HttpConnectionContext _context; + private ReusableHttpContext _httpContext; protected string _methodText = null; private string _scheme = null; @@ -275,6 +277,23 @@ public CancellationToken RequestAborted protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); + HttpContext IHttpContextContainer.HttpContext + { + get + { + if (_httpContext is null) + { + _httpContext = new ReusableHttpContext(this); + } + else + { + _httpContext.Initialize(this); + } + + return _httpContext; + } + } + public void InitializeStreams(MessageBody messageBody) { if (_streams == null) @@ -359,6 +378,8 @@ public void Reset() _responseBytesWritten = 0; + _httpContext?.Uninitialize(); + OnReset(); } @@ -535,14 +556,14 @@ private async Task ProcessRequests(IHttpApplication applicat InitializeStreams(messageBody); - var httpContext = application.CreateContext(this); + var context = application.CreateContext(this); try { KestrelEventSource.Log.RequestStart(this); // Run the application code for this request - await application.ProcessRequestAsync(httpContext); + await application.ProcessRequestAsync(context); if (!_requestAborted) { @@ -609,7 +630,7 @@ private async Task ProcessRequests(IHttpApplication applicat await FireOnCompleted(); } - application.DisposeContext(httpContext, _applicationException); + application.DisposeContext(context, _applicationException); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. if (!_requestAborted && _requestRejectedException == null && !messageBody.IsEmpty)