From c6c5dd6fbfc1259e2f1b009ffe979b13be91ee60 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 20 Feb 2014 14:51:31 -0800 Subject: [PATCH] Re-layer the feature interfaces. --- .../OwinWebListener.cs | 3 +- .../RequestProcessing/FeatureContext.cs | 338 ++++++++++++++++++ .../RequestProcessing/Request.cs | 217 ++--------- .../RequestProcessing/RequestContext.cs | 43 +-- .../RequestProcessing/Response.cs | 41 +-- .../RequestProcessing/ResponseStream.cs | 2 + 6 files changed, 393 insertions(+), 251 deletions(-) create mode 100644 src/Microsoft.AspNet.Server.WebListener/RequestProcessing/FeatureContext.cs diff --git a/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs b/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs index 5f7b8c3ffb..4220f1c4fc 100644 --- a/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs +++ b/src/Microsoft.AspNet.Server.WebListener/OwinWebListener.cs @@ -518,7 +518,8 @@ namespace Microsoft.AspNet.Server.WebListener { // TODO: Make disconnect registration lazy RegisterForDisconnectNotification(requestContext); - await _appFunc(requestContext.Features).SupressContext(); + FeatureContext featureContext = new FeatureContext(requestContext); + await _appFunc(featureContext.Features).SupressContext(); await requestContext.ProcessResponseAsync().SupressContext(); } catch (Exception ex) diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/FeatureContext.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/FeatureContext.cs new file mode 100644 index 0000000000..786256daab --- /dev/null +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/FeatureContext.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +#if NET45 +using System.Security.Cryptography.X509Certificates; +#endif +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.FeatureModel; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.Server.WebListener +{ + internal class FeatureContext : IHttpRequestInformation, IHttpConnection, IHttpResponseInformation, IHttpSendFile +#if NET45 + , IHttpTransportLayerSecurity +#endif + { + private RequestContext _requestContext; + private FeatureCollection _features; + + private Stream _requestBody; + private IDictionary _requestHeaders; + private string _scheme; + private string _httpMethod; + private string _httpProtocolVersion; + private string _query; + private string _pathBase; + private string _path; +#if NET45 + private IPAddress _remoteIpAddress; + private IPAddress _localIpAddress; +#endif + private int? _remotePort; + private int? _localPort; + private bool? _isLocal; +#if NET45 + private X509Certificate _clientCert; +#endif + private Stream _responseStream; + private IDictionary _responseHeaders; + + internal FeatureContext(RequestContext requestContext) + { + _requestContext = requestContext; + _features = new FeatureCollection(); + PopulateFeatures(); + } + + internal IFeatureCollection Features + { + get { return _features; } + } + + private Request Request + { + get { return _requestContext.Request; } + } + + private Response Response + { + get { return _requestContext.Response; } + } + + private void PopulateFeatures() + { + _features.Add(typeof(IHttpRequestInformation), this); + _features.Add(typeof(IHttpConnection), this); + if (Request.IsSecureConnection) + { +#if NET45 + // TODO: Should this feature be conditional? Should we add this for HTTP requests? + _features.Add(typeof(IHttpTransportLayerSecurity), this); +#endif + } + _features.Add(typeof(IHttpResponseInformation), this); + _features.Add(typeof(IHttpSendFile), this); + + // TODO: + // _environment.CallCancelled = _cts.Token; + // _environment.User = _request.User; + // Opaque/WebSockets + // Channel binding + + /* + // Server + _environment.Listener = _server; + _environment.ConnectionId = _request.ConnectionId; + */ + } + +#region IHttpRequestInformation + + Stream IHttpRequestInformation.Body + { + get + { + if (_requestBody == null) + { + _requestBody = Request.Body; + } + return _requestBody; + } + set { _requestBody = value; } + } + + IDictionary IHttpRequestInformation.Headers + { + get + { + if (_requestHeaders == null) + { + _requestHeaders = Request.Headers; + } + return _requestHeaders; + } + set { _requestHeaders = value; } + } + + string IHttpRequestInformation.Method + { + get + { + if (_httpMethod == null) + { + _httpMethod = Request.Method; + } + return _httpMethod; + } + set { _httpMethod = value; } + } + + string IHttpRequestInformation.Path + { + get + { + if (_path == null) + { + _path = Request.Path; + } + return _path; + } + set { _path = value; } + } + + string IHttpRequestInformation.PathBase + { + get + { + if (_pathBase == null) + { + _pathBase = Request.PathBase; + } + return _pathBase; + } + set { _pathBase = value; } + } + + string IHttpRequestInformation.Protocol + { + get + { + if (_httpProtocolVersion == null) + { + _httpProtocolVersion = Request.Protocol; + } + return _httpProtocolVersion; + } + set { _httpProtocolVersion = value; } + } + + string IHttpRequestInformation.QueryString + { + get + { + if (_query == null) + { + _query = Request.QueryString; + } + return _query; + } + set { _query = value; } + } + + string IHttpRequestInformation.Scheme + { + get + { + if (_scheme == null) + { + _scheme = Request.Scheme; + } + return _scheme; + } + set { _scheme = value; } + } +#endregion +#region IHttpConnection + bool IHttpConnection.IsLocal + { + get + { + if (_isLocal == null) + { + _isLocal = Request.IsLocal; + } + return _isLocal.Value; + } + set { _isLocal = value; } + } +#if NET45 + IPAddress IHttpConnection.LocalIpAddress + { + get + { + if (_localIpAddress == null) + { + _localIpAddress = Request.LocalIpAddress; + } + return _localIpAddress; + } + set { _localIpAddress = value; } + } + + IPAddress IHttpConnection.RemoteIpAddress + { + get + { + if (_remoteIpAddress == null) + { + _remoteIpAddress = Request.RemoteIpAddress; + } + return _remoteIpAddress; + } + set { _remoteIpAddress = value; } + } +#endif + int IHttpConnection.LocalPort + { + get + { + if (_localPort == null) + { + _localPort = Request.LocalPort; + } + return _localPort.Value; + } + set { _localPort = value; } + } + + int IHttpConnection.RemotePort + { + get + { + if (_remotePort == null) + { + _remotePort = Request.RemotePort; + } + return _remotePort.Value; + } + set { _remotePort = value; } + } +#endregion +#region IHttpTransportLayerSecurity +#if NET45 + X509Certificate IHttpTransportLayerSecurity.ClientCertificate + { + get + { + if (_clientCert == null) + { + _clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync; + } + return _clientCert; + } + set { _clientCert = value; } + } + + async Task IHttpTransportLayerSecurity.LoadAsync() + { + if (_clientCert == null) + { + _clientCert = await Request.GetClientCertificateAsync(); + } + } +#endif +#endregion +#region IHttpResponseInformation + Stream IHttpResponseInformation.Body + { + get + { + if (_responseStream == null) + { + _responseStream = Response.Body; + } + return _responseStream; + } + set { _responseStream = value; } + } + + IDictionary IHttpResponseInformation.Headers + { + get + { + if (_responseHeaders == null) + { + _responseHeaders = Response.Headers; + } + return _responseHeaders; + } + set { _responseHeaders = value; } + } + + void IHttpResponseInformation.OnSendingHeaders(Action callback, object state) + { + Response.OnSendingHeaders(callback, state); + } + + string IHttpResponseInformation.ReasonPhrase + { + get { return Response.ReasonPhrase; } + set { Response.ReasonPhrase = value; } + } + + int IHttpResponseInformation.StatusCode + { + get { return Response.StatusCode; } + set { Response.StatusCode = value; } + } +#endregion + Task IHttpSendFile.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + return Response.SendFileAsync(path, offset, length, cancellation); + } + } +} diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Request.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Request.cs index 5505e44ef0..0a3a186d98 100644 --- a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Request.cs +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Request.cs @@ -11,17 +11,15 @@ using System.IO; using System.Net; using System.Runtime.InteropServices; #if NET45 -using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography.X509Certificates; #endif using System.Security.Principal; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.HttpFeature; namespace Microsoft.AspNet.Server.WebListener { - internal sealed class Request : IHttpRequestInformation, IHttpConnection, IHttpTransportLayerSecurity, IDisposable + internal sealed class Request : IDisposable { private RequestContext _requestContext; private NativeRequestContext _nativeRequestContext; @@ -31,11 +29,9 @@ namespace Microsoft.AspNet.Server.WebListener private ulong _contextId; private SslStatus _sslStatus; - private string _scheme; private string _httpMethod; private Version _httpVersion; - private string _httpProtocolVersion; // private Uri _requestUri; private string _rawUrl; @@ -53,17 +49,9 @@ namespace Microsoft.AspNet.Server.WebListener private BoundaryType _contentBoundaryType; private long _contentLength; private Stream _nativeStream; - private Stream _requestStream; private SocketAddress _localEndPoint; private SocketAddress _remoteEndPoint; -#if NET45 - private IPAddress _remoteIpAddress; - private IPAddress _localIpAddress; -#endif - private int? _remotePort; - private int? _localPort; - private bool? _isLocal; private IPrincipal _user; @@ -215,25 +203,6 @@ namespace Microsoft.AspNet.Server.WebListener } } -#if NET45 - X509Certificate IHttpTransportLayerSecurity.ClientCertificate - { - get - { - if (_clientCert == null) - { - // TODO: Sync - ((IHttpTransportLayerSecurity)this).LoadAsync().Wait(); - } - return _clientCert; - } - set - { - _clientCert = value; - } - } -#endif - // TODO: Move this to the constructor, that's where it will be called. internal long ContentLength64 { @@ -270,33 +239,15 @@ namespace Microsoft.AspNet.Server.WebListener public IDictionary Headers { - get - { - return _headers; - } - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - _headers = value; - } + get { return _headers; } } public string Method { - get - { - return _httpMethod; - } - set - { - _httpMethod = value; - } + get { return _httpMethod; } } - internal Stream NativeStream + public Stream Body { get { @@ -309,60 +260,25 @@ namespace Microsoft.AspNet.Server.WebListener } } - public Stream Body - { - get - { - if (_requestStream == null) - { - // TODO: Move this to the constructor (or a lazy Env dictionary) - _requestStream = NativeStream; - } - return _requestStream; - } - set - { - _requestStream = value; - } - } - public string PathBase { - get - { - return _pathBase; - } - set - { - _pathBase = value; - } + get { return _pathBase; } } public string Path { - get - { - return _path; - } - set - { - _path = value; - } + get { return _path; } } public bool IsLocal { get { - if (!_isLocal.HasValue) - { - _isLocal = LocalEndPoint.GetIPAddress().Equals(RemoteEndPoint.GetIPAddress()); - } - return _isLocal.Value; - } - set - { - _isLocal = value; +#if NET45 + return LocalEndPoint.GetIPAddress().Equals(RemoteEndPoint.GetIPAddress()); +#else + throw new NotImplementedException(); +#endif } } @@ -394,30 +310,19 @@ namespace Microsoft.AspNet.Server.WebListener { get { - if (_httpProtocolVersion == null) + if (_httpVersion.Major == 1) { - if (_httpVersion.Major == 1) + if (_httpVersion.Minor == 1) { - if (_httpVersion.Minor == 1) - { - _httpProtocolVersion = "HTTP/1.1"; - } - else if (_httpVersion.Minor == 0) - { - _httpProtocolVersion = "HTTP/1.0"; - } + return "HTTP/1.1"; } - else + else if (_httpVersion.Minor == 0) { - _httpProtocolVersion = "HTTP/" + _httpVersion.ToString(2); + return "HTTP/1.0"; } } - return _httpProtocolVersion; - } - set - { - // TODO: Set _httpVersion? - _httpProtocolVersion = value; + + return "HTTP/" + _httpVersion.ToString(2); } } @@ -432,7 +337,7 @@ namespace Microsoft.AspNet.Server.WebListener } } - internal SocketAddress RemoteEndPoint + private SocketAddress RemoteEndPoint { get { @@ -445,7 +350,7 @@ namespace Microsoft.AspNet.Server.WebListener } } - internal SocketAddress LocalEndPoint + private SocketAddress LocalEndPoint { get { @@ -457,85 +362,30 @@ namespace Microsoft.AspNet.Server.WebListener return _localEndPoint; } } - +#if NET45 public IPAddress RemoteIpAddress { - get - { - if (_remoteIpAddress == null) - { - _remoteIpAddress = RemoteEndPoint.GetIPAddress(); - } - return _remoteIpAddress; - } - set - { - _remoteIpAddress = value; - } + get { return RemoteEndPoint.GetIPAddress(); } } public IPAddress LocalIpAddress { - get - { - if (_localIpAddress == null) - { - _localIpAddress = LocalEndPoint.GetIPAddress(); - } - return _localIpAddress; - } - set - { - _localIpAddress = value; - } + get { return LocalEndPoint.GetIPAddress(); } } - +#endif public int RemotePort { - get - { - if (!_remotePort.HasValue) - { - _remotePort = RemoteEndPoint.GetPort(); - } - return _remotePort.Value; - } - set - { - _remotePort = value; - } + get { return RemoteEndPoint.GetPort(); } } public int LocalPort { - get - { - if (!_localPort.HasValue) - { - _localPort = LocalEndPoint.GetPort(); - } - return _localPort.Value; - } - set - { - _localPort = value; - } + get { return LocalEndPoint.GetPort(); } } public string Scheme { - get - { - if (_scheme == null) - { - _scheme = IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; - } - return _scheme; - } - set - { - _scheme = value; - } + get { return IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; } } /* internal Uri RequestUri @@ -594,21 +444,21 @@ namespace Microsoft.AspNet.Server.WebListener #endif } +#if NET45 // Populates the client certificate. The result may be null if there is no client cert. // TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to // enable this, but it's unclear what Http.Sys would do. - async Task IHttpTransportLayerSecurity.LoadAsync() + public async Task GetClientCertificateAsync() { if (SslStatus == SslStatus.Insecure) { // Non-SSL - return; + return null; } // TODO: Verbose log -#if NET45 if (_clientCert != null) { - return; + return _clientCert; } ClientCertLoader certLoader = new ClientCertLoader(RequestContext); @@ -630,10 +480,9 @@ namespace Microsoft.AspNet.Server.WebListener } throw; } -#else - throw new NotImplementedException(); -#endif + return _clientCert; } +#endif // Use this to save the blob from dispose if this object was never used (never given to a user) and is about to be // disposed. diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/RequestContext.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/RequestContext.cs index 934ed1eb87..654772e4ad 100644 --- a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/RequestContext.cs +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/RequestContext.cs @@ -8,15 +8,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.IO; using System.Runtime.InteropServices; -using System.Security.Authentication.ExtendedProtection; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; namespace Microsoft.AspNet.Server.WebListener { @@ -27,7 +21,6 @@ namespace Microsoft.AspNet.Server.WebListener { private static readonly string[] ZeroContentLength = new[] { "0" }; - private FeatureCollection _features; private OwinWebListener _server; private Request _request; private Response _response; @@ -44,18 +37,9 @@ namespace Microsoft.AspNet.Server.WebListener _request = new Request(this, _memoryBlob); _response = new Response(this); _cts = new CancellationTokenSource(); - - _features = new FeatureCollection(); - PopulateFeatures(); - _request.ReleasePins(); } - internal IFeatureCollection Features - { - get { return _features; } - } - internal Request Request { get @@ -100,31 +84,6 @@ namespace Microsoft.AspNet.Server.WebListener return Request.RequestId; } } - - private void PopulateFeatures() - { - _features.Add(typeof(IHttpRequestInformation), Request); - _features.Add(typeof(IHttpConnection), Request); - if (Request.IsSecureConnection) - { - // TODO: Should this feature be conditional? Should we add this for HTTP requests? - _features.Add(typeof(IHttpTransportLayerSecurity), Request); - } - _features.Add(typeof(IHttpResponseInformation), Response); - _features.Add(typeof(IHttpSendFile), Response); - - // TODO: - // _environment.CallCancelled = _cts.Token; - // _environment.User = _request.User; - // Opaque/WebSockets - // Channel binding - - /* - // Server - _environment.Listener = _server; - _environment.ConnectionId = _request.ConnectionId; - */ - } /* public bool TryGetOpaqueUpgrade(ref Action, OpaqueFunc> value) { @@ -265,7 +224,7 @@ namespace Microsoft.AspNet.Server.WebListener Request.SwitchToOpaqueMode(); Response.SwitchToOpaqueMode(); - opaqueEnv[Constants.OpaqueStreamKey] = new OpaqueStream(Request.NativeStream, Response.NativeStream); + opaqueEnv[Constants.OpaqueStreamKey] = new OpaqueStream(Request.Body, Response.Body); return opaqueEnv; } diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs index 9f2ac4807d..4988fd0e30 100644 --- a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/Response.cs @@ -8,23 +8,19 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.HttpFeature; namespace Microsoft.AspNet.Server.WebListener { - internal sealed unsafe class Response : IHttpResponseInformation, IHttpSendFile, IDisposable + internal sealed unsafe class Response : IDisposable { private ResponseState _responseState; private IDictionary _headers; private string _reasonPhrase; private ResponseStream _nativeStream; - private Stream _responseStream; private long _contentLength; private BoundaryType _boundaryType; private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE _nativeResponse; @@ -79,17 +75,31 @@ namespace Microsoft.AspNet.Server.WebListener { throw new ArgumentOutOfRangeException("value", value, string.Format(Resources.Exception_InvalidStatusCode, value)); } + CheckResponseStarted(); _nativeResponse.StatusCode = (ushort)value; } } + private void CheckResponseStarted() + { + if (_responseState >= ResponseState.SentHeaders) + { + throw new InvalidOperationException("Headers already sent."); + } + } + public string ReasonPhrase { get { return _reasonPhrase; } - set { _reasonPhrase = value; } + set + { + // TODO: Validate user input for illegal chars, length limit, etc.? + CheckResponseStarted(); + _reasonPhrase = value; + } } - internal ResponseStream NativeStream + internal ResponseStream Body { get { @@ -99,25 +109,8 @@ namespace Microsoft.AspNet.Server.WebListener } } - public Stream Body - { - get - { - if (_responseStream == null) - { - _responseStream = NativeStream; - } - return _responseStream; - } - set - { - _responseStream = value; - } - } - internal string GetReasonPhrase(int statusCode) { - // TODO: Validate user input for illegal chars, length limit, etc.? string reasonPhrase = ReasonPhrase; if (string.IsNullOrWhiteSpace(reasonPhrase)) { diff --git a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/ResponseStream.cs b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/ResponseStream.cs index 84c29adefe..45c3d698ff 100644 --- a/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/ResponseStream.cs +++ b/src/Microsoft.AspNet.Server.WebListener/RequestProcessing/ResponseStream.cs @@ -89,6 +89,7 @@ namespace Microsoft.AspNet.Server.WebListener uint statusCode; unsafe { + // TODO: Don't add MoreData flag if content-length == 0? flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; statusCode = _requestContext.Response.SendHeaders(null, null, flags, false); } @@ -120,6 +121,7 @@ namespace Microsoft.AspNet.Server.WebListener // TODO: Real cancellation cancellationToken.ThrowIfCancellationRequested(); + // TODO: Don't add MoreData flag if content-length == 0? flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA; ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, null, 0, 0, _requestContext.Response.BoundaryType == BoundaryType.Chunked, false);