#121 Enable kernel mode response caching.
This commit is contained in:
parent
3c044fb92e
commit
20f2219886
|
|
@ -27,6 +27,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
using Microsoft.AspNet.Http.Features.Authentication;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.Net.Http.Server;
|
||||
using Microsoft.Net.WebSockets;
|
||||
|
||||
|
|
@ -46,8 +47,11 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
IHttpUpgradeFeature,
|
||||
IRequestIdentifierFeature
|
||||
{
|
||||
private static Action<object> OnStartDelegate = OnStart;
|
||||
|
||||
private RequestContext _requestContext;
|
||||
private FeatureCollection _features;
|
||||
private bool _enableResponseCaching;
|
||||
|
||||
private Stream _requestBody;
|
||||
private IDictionary<string, string[]> _requestHeaders;
|
||||
|
|
@ -69,11 +73,13 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
private Stream _responseStream;
|
||||
private IDictionary<string, string[]> _responseHeaders;
|
||||
|
||||
internal FeatureContext(RequestContext requestContext)
|
||||
internal FeatureContext(RequestContext requestContext, bool enableResponseCaching)
|
||||
{
|
||||
_requestContext = requestContext;
|
||||
_features = new FeatureCollection();
|
||||
_authHandler = new AuthenticationHandler(requestContext);
|
||||
_enableResponseCaching = enableResponseCaching;
|
||||
requestContext.Response.OnSendingHeaders(OnStartDelegate, this);
|
||||
PopulateFeatures();
|
||||
}
|
||||
|
||||
|
|
@ -471,5 +477,69 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
return _requestContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnStart(object obj)
|
||||
{
|
||||
var featureContext = (FeatureContext)obj;
|
||||
|
||||
ConsiderEnablingResponseCache(featureContext);
|
||||
}
|
||||
|
||||
private static void ConsiderEnablingResponseCache(FeatureContext featureContext)
|
||||
{
|
||||
if (featureContext._enableResponseCaching)
|
||||
{
|
||||
// We don't have to worry too much about what Http.Sys supports, caching is a best-effort feature.
|
||||
// If there's something about the request or response that prevents it from caching then the response
|
||||
// will complete normally without caching.
|
||||
featureContext._requestContext.Response.CacheTtl = GetCacheTtl(featureContext._requestContext);
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan? GetCacheTtl(RequestContext requestContext)
|
||||
{
|
||||
var response = requestContext.Response;
|
||||
// Only consider kernel-mode caching if the Cache-Control response header is present.
|
||||
var cacheControlHeader = response.Headers[HeaderNames.CacheControl];
|
||||
if (string.IsNullOrEmpty(cacheControlHeader))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Before we check the header value, check for the existence of other headers which would
|
||||
// make us *not* want to cache the response.
|
||||
if (response.Headers.ContainsKey(HeaderNames.SetCookie)
|
||||
|| response.Headers.ContainsKey(HeaderNames.Vary)
|
||||
|| response.Headers.ContainsKey(HeaderNames.Pragma))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// We require 'public' and 's-max-age' or 'max-age' or the Expires header.
|
||||
CacheControlHeaderValue cacheControl;
|
||||
if (CacheControlHeaderValue.TryParse(cacheControlHeader, out cacheControl) && cacheControl.Public)
|
||||
{
|
||||
if (cacheControl.SharedMaxAge.HasValue)
|
||||
{
|
||||
return cacheControl.SharedMaxAge;
|
||||
}
|
||||
else if (cacheControl.MaxAge.HasValue)
|
||||
{
|
||||
return cacheControl.MaxAge;
|
||||
}
|
||||
|
||||
DateTimeOffset expirationDate;
|
||||
if (HeaderUtilities.TryParseDate(response.Headers[HeaderNames.Expires], out expirationDate))
|
||||
{
|
||||
var expiresOffset = expirationDate - DateTimeOffset.UtcNow;
|
||||
if (expiresOffset > TimeSpan.Zero)
|
||||
{
|
||||
return expiresOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
}
|
||||
}
|
||||
|
||||
internal bool EnableResponseCaching { get; set; } = true;
|
||||
|
||||
internal void Start(AppFunc app)
|
||||
{
|
||||
// Can't call Start twice
|
||||
|
|
@ -160,7 +162,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
try
|
||||
{
|
||||
Interlocked.Increment(ref _outstandingRequests);
|
||||
FeatureContext featureContext = new FeatureContext(requestContext);
|
||||
FeatureContext featureContext = new FeatureContext(requestContext, EnableResponseCaching);
|
||||
await _appFunc(featureContext.Features).SupressContext();
|
||||
requestContext.Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,5 +50,11 @@ namespace Microsoft.AspNet.Server.WebListener
|
|||
get { return _messagePump.MaxAccepts; }
|
||||
set { _messagePump.MaxAccepts = value; }
|
||||
}
|
||||
|
||||
public bool EnableResponseCaching
|
||||
{
|
||||
get { return _messagePump.EnableResponseCaching; }
|
||||
set { _messagePump.EnableResponseCaching = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"Microsoft.AspNet.Http.Features": "1.0.0-*",
|
||||
"Microsoft.AspNet.Hosting.Server.Abstractions": "1.0.0-*",
|
||||
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
|
||||
"Microsoft.Net.Http.Headers": "1.0.0-*",
|
||||
"Microsoft.Net.Http.Server": "1.0.0-*"
|
||||
},
|
||||
"compilationOptions": {
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ namespace Microsoft.Net.Http.Server
|
|||
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
|
||||
|
||||
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, void* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
|
||||
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, HTTP_CACHE_POLICY* pCachePolicy, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
|
||||
|
||||
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||
internal static extern uint HttpSendResponseEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, ushort entityChunkCount, HTTP_DATA_CHUNK* pEntityChunks, uint* pBytesSent, SafeLocalFree pRequestBuffer, uint requestBufferLength, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
|
||||
|
|
@ -369,6 +369,21 @@ namespace Microsoft.Net.Http.Server
|
|||
internal ushort* pQueryString;
|
||||
}
|
||||
|
||||
// Only cache unauthorized GETs + HEADs.
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct HTTP_CACHE_POLICY
|
||||
{
|
||||
internal HTTP_CACHE_POLICY_TYPE Policy;
|
||||
internal uint SecondsToLive;
|
||||
}
|
||||
|
||||
internal enum HTTP_CACHE_POLICY_TYPE : int
|
||||
{
|
||||
HttpCachePolicyNocache = 0,
|
||||
HttpCachePolicyUserInvalidates = 1,
|
||||
HttpCachePolicyTimeToLive = 2,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct SOCKADDR
|
||||
{
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ namespace Microsoft.Net.Http.Server
|
|||
_bufferingEnabled = _requestContext.Server.BufferResponses;
|
||||
_expectedBodyLength = 0;
|
||||
_nativeStream = null;
|
||||
CacheTtl = null;
|
||||
}
|
||||
|
||||
private enum ResponseState
|
||||
|
|
@ -306,6 +307,8 @@ namespace Microsoft.Net.Http.Server
|
|||
get { return _responseState >= ResponseState.StartedSending; }
|
||||
}
|
||||
|
||||
public TimeSpan? CacheTtl { get; set; }
|
||||
|
||||
private void EnsureResponseStream()
|
||||
{
|
||||
if (_nativeStream == null)
|
||||
|
|
@ -391,6 +394,14 @@ namespace Microsoft.Net.Http.Server
|
|||
_nativeResponse.Response_V1.pEntityChunks = null;
|
||||
}
|
||||
|
||||
var cachePolicy = new HttpApi.HTTP_CACHE_POLICY();
|
||||
var cacheTtl = CacheTtl;
|
||||
if (cacheTtl.HasValue && cacheTtl.Value > TimeSpan.Zero)
|
||||
{
|
||||
cachePolicy.Policy = HttpApi.HTTP_CACHE_POLICY_TYPE.HttpCachePolicyTimeToLive;
|
||||
cachePolicy.SecondsToLive = (uint)Math.Min(cacheTtl.Value.Ticks / TimeSpan.TicksPerSecond, Int32.MaxValue);
|
||||
}
|
||||
|
||||
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
|
||||
fixed (byte* pReasonPhrase = reasonPhraseBytes)
|
||||
{
|
||||
|
|
@ -405,7 +416,7 @@ namespace Microsoft.Net.Http.Server
|
|||
Request.RequestId,
|
||||
(uint)flags,
|
||||
pResponse,
|
||||
null,
|
||||
&cachePolicy,
|
||||
&bytesSent,
|
||||
SafeLocalFree.Zero,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -583,7 +583,6 @@ namespace Microsoft.Net.Http.Server
|
|||
|
||||
uint statusCode;
|
||||
uint bytesSent = 0;
|
||||
flags |= _leftToWrite == count ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
||||
bool startedSending = _requestContext.Response.HasStartedSending;
|
||||
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
||||
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration);
|
||||
|
|
@ -602,6 +601,7 @@ namespace Microsoft.Net.Http.Server
|
|||
bytesWritten = asyncResult.FileLength - offset;
|
||||
}
|
||||
// Update _leftToWrite now so we can queue up additional calls to SendFileAsync.
|
||||
flags |= _leftToWrite == bytesWritten ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
||||
UpdateWritenCount((uint)bytesWritten);
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FeatureModel;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Server.WebListener.FunctionalTests
|
||||
{
|
||||
public class ResponseCachingTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Caching_NoCacheControl_NotCached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("2", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_JustPublic_NotCached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public";
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("2", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_MaxAge_Cached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_SMaxAge_Cached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public, s-maxage=10";
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_SMaxAgeAndMaxAge_SMaxAgePreferredCached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public, max-age=0, s-maxage=10";
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_Expires_Cached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public";
|
||||
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Set-cookie")]
|
||||
[InlineData("vary")]
|
||||
[InlineData("pragma")]
|
||||
public async Task Caching_DisallowedResponseHeaders_NotCached(string headerName)
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
|
||||
httpContext.Response.Headers[headerName] = "headerValue";
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("2", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0")]
|
||||
[InlineData("-1")]
|
||||
public async Task Caching_InvalidExpires_NotCached(string expiresValue)
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public";
|
||||
httpContext.Response.Headers["Expires"] = expiresValue;
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("2", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_ExpiresWithoutPublic_NotCached()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("2", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Caching_MaxAgeAndExpires_MaxAgePreferred()
|
||||
{
|
||||
var requestCount = 1;
|
||||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, env =>
|
||||
{
|
||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||
httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
|
||||
httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
|
||||
httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
|
||||
httpContext.Response.Headers["Expires"] = (DateTime.UtcNow - TimeSpan.FromSeconds(10)).ToString("r"); // In the past
|
||||
return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||
}))
|
||||
{
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
Assert.Equal("1", await SendRequestAsync(address));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> SendRequestAsync(string uri)
|
||||
{
|
||||
using (var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) })
|
||||
{
|
||||
var response = await client.GetAsync(uri);
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
Assert.Equal(10, response.Content.Headers.ContentLength);
|
||||
Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
|
||||
return response.Headers.GetValues("x-request-count").FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,8 @@
|
|||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue