// Copyright (c) .NET Foundation. 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.Generic; using System.Globalization; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Xunit; namespace Microsoft.AspNetCore.StaticFiles { public class CacheHeaderTests { [Fact] public async Task ServerShouldReturnETag() { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); Assert.NotNull(response.Headers.ETag); Assert.NotNull(response.Headers.ETag.Tag); } [Fact] public async Task SameETagShouldBeReturnedAgain() { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage response1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); HttpResponseMessage response2 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); Assert.Equal(response2.Headers.ETag, response1.Headers.ETag); } // 14.24 If-Match // If none of the entity tags match, or if "*" is given and no current // entity exists, the server MUST NOT perform the requested method, and // MUST return a 412 (Precondition Failed) response. This behavior is // most useful when the client wants to prevent an updating method, such // as PUT, from modifying a resource that has changed since the client // last retrieved it. [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfMatchShouldReturn412WhenNotListed(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", "\"fake\""); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.PreconditionFailed, resp.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfMatchShouldBeServedWhenListed(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", original.Headers.ETag.ToString()); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfMatchShouldBeServedForAstrisk(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", "*"); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); } [Theory] [MemberData(nameof(UnsupportedMethods))] public async Task IfMatchShouldBeIgnoredForUnsupportedMethods(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", "*"); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); } // 14.26 If-None-Match // If any of the entity tags match the entity tag of the entity that // would have been returned in the response to a similar GET request // (without the If-None-Match header) on that resource, or if "*" is // given and any current entity exists for that resource, then the // server MUST NOT perform the requested method, unless required to do // so because the resource's modification date fails to match that // supplied in an If-Modified-Since header field in the request. // Instead, if the request method was GET or HEAD, the server SHOULD // respond with a 304 (Not Modified) response, including the cache- // related header fields (particularly ETag) of one of the entities that // matched. For all other request methods, the server MUST respond with // a status of 412 (Precondition Failed). [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfNoneMatchShouldReturn304ForMatching(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfNoneMatchAllShouldReturn304ForMatching(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req2.Headers.Add("If-None-Match", "*"); HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); } [Theory] [MemberData(nameof(UnsupportedMethods))] public async Task IfNoneMatchShouldBeIgnoredForNonTwoHundredAnd304Responses(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); Assert.Equal(HttpStatusCode.NotFound, resp2.StatusCode); } // 14.26 If-None-Match // If none of the entity tags match, then the server MAY perform the // requested method as if the If-None-Match header field did not exist, // but MUST also ignore any If-Modified-Since header field(s) in the // request. That is, if no entity tags match, then the server MUST NOT // return a 304 (Not Modified) response. // A server MUST use the strong comparison function (see section 13.3.3) // to compare the entity tags in If-Match. [Theory] [MemberData(nameof(SupportedMethods))] public async Task ServerShouldReturnLastModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage response = await server.CreateClient().SendAsync( new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml")); Assert.NotNull(response.Content.Headers.LastModified); // Verify that DateTimeOffset is UTC Assert.Equal(response.Content.Headers.LastModified.Value.Offset, TimeSpan.Zero); } // 13.3.4 // An HTTP/1.1 origin server, upon receiving a conditional request that // includes both a Last-Modified date (e.g., in an If-Modified-Since or // If-Unmodified-Since header field) and one or more entity tags (e.g., // in an If-Match, If-None-Match, or If-Range header field) as cache // validators, MUST NOT return a response status of 304 (Not Modified) // unless doing so is consistent with all of the conditional header // fields in the request. [Theory] [MemberData(nameof(SupportedMethods))] public async Task MatchingBothConditionsReturnsNotModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); HttpResponseMessage resp2 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", resp1.Headers.ETag.ToString()) .And(req => req.Headers.IfModifiedSince = resp1.Content.Headers.LastModified) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task MatchingAtLeastOneETagReturnsNotModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); var etag = resp1.Headers.ETag.ToString(); HttpResponseMessage resp2 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Match", etag + ", " + etag) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, resp2.StatusCode); HttpResponseMessage resp3 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Match", etag+ ", \"badetag\"") .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, resp3.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task MissingEitherOrBothConditionsReturnsNormally(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); DateTimeOffset lastModified = resp1.Content.Headers.LastModified.Value; DateTimeOffset pastDate = lastModified.AddHours(-1); DateTimeOffset furtureDate = lastModified.AddHours(1); HttpResponseMessage resp2 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", "\"fake\"") .And(req => req.Headers.IfModifiedSince = lastModified) .SendAsync(method.Method); HttpResponseMessage resp3 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", resp1.Headers.ETag.ToString()) .And(req => req.Headers.IfModifiedSince = pastDate) .SendAsync(method.Method); HttpResponseMessage resp4 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", "\"fake\"") .And(req => req.Headers.IfModifiedSince = furtureDate) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, resp2.StatusCode); Assert.Equal(HttpStatusCode.OK, resp3.StatusCode); Assert.Equal(HttpStatusCode.OK, resp4.StatusCode); } // 14.25 If-Modified-Since // The If-Modified-Since request-header field is used with a method to // make it conditional: if the requested variant has not been modified // since the time specified in this field, an entity will not be // returned from the server; instead, a 304 (not modified) response will // be returned without any message-body. // a) If the request would normally result in anything other than a // 200 (OK) status, or if the passed If-Modified-Since date is // invalid, the response is exactly the same as for a normal GET. // A date which is later than the server's current time is // invalid. [Theory] [MemberData(nameof(SupportedMethods))] public async Task InvalidIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Modified-Since", "bad-date") .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task FutureIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now.AddYears(1)) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } // b) If the variant has been modified since the If-Modified-Since // date, the response is exactly the same as for a normal GET. // c) If the variant has not been modified since a valid If- // Modified-Since date, the server SHOULD return a 304 (Not // Modified) response. [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfModifiedSinceDateGreaterThanLastModifiedShouldReturn304(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task SuppportsIfModifiedDateFormats(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); var formats = new[] { "ddd, dd MMM yyyy HH:mm:ss 'GMT'", "dddd, dd-MMM-yy HH:mm:ss 'GMT'", "ddd MMM d HH:mm:ss yyyy" }; foreach (var format in formats) { HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Modified-Since", DateTimeOffset.UtcNow.ToString(format)) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode); } } [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfModifiedSinceDateLessThanLastModifiedShouldReturn200(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfModifiedSince = DateTimeOffset.MinValue) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res2.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task InvalidIfUnmodifiedSinceDateFormatGivesNormalGet(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Unmodified-Since", "bad-date") .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task FutureIfUnmodifiedSinceDateFormatGivesNormalGet(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfUnmodifiedSince = DateTimeOffset.Now.AddYears(1)) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } [Theory] [MemberData(nameof(SupportedMethods))] public async Task IfUnmodifiedSinceDateLessThanLastModifiedShouldReturn412(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") .SendAsync(method.Method); HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfUnmodifiedSince = DateTimeOffset.MinValue) .SendAsync(method.Method); Assert.Equal(HttpStatusCode.PreconditionFailed, res2.StatusCode); } public static IEnumerable SupportedMethods => new[] { new [] { HttpMethod.Get }, new [] { HttpMethod.Head } }; public static IEnumerable UnsupportedMethods => new[] { new [] { HttpMethod.Post }, new [] { HttpMethod.Put }, new [] { HttpMethod.Options }, new [] { HttpMethod.Trace }, new [] { new HttpMethod("VERB") } }; } }