diff --git a/samples/ResponseCompressionSample/Startup.cs b/samples/ResponseCompressionSample/Startup.cs index e10baad03c..8489864aea 100644 --- a/samples/ResponseCompressionSample/Startup.cs +++ b/samples/ResponseCompressionSample/Startup.cs @@ -26,6 +26,11 @@ namespace ResponseCompressionSample options.Providers.Add(); // .Append(TItem) is only available on Core. options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" }); + + ////Example of using excluded and wildcard MIME types: + ////Compress all MIME types except various media types, but do compress SVG images. + //options.MimeTypes = new[] { "*/*", "image/svg+xml" }; + //options.ExcludedMimeTypes = new[] { "image/*", "audio/*", "video/*" }; }); } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs index 6ec4fb62b4..45168d04d5 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs @@ -15,9 +15,14 @@ namespace Microsoft.AspNetCore.ResponseCompression /// public IEnumerable MimeTypes { get; set; } + /// + /// Response Content-Type MIME types to not compress. + /// + public IEnumerable ExcludedMimeTypes { get; set; } + /// /// Indicates if responses over HTTPS connections should be compressed. The default is 'false'. - /// Enable compression on HTTPS connections may expose security problems. + /// Enabling compression on HTTPS connections may expose security problems. /// public bool EnableForHttps { get; set; } = false; diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs index 33b234091e..d459c13ff3 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCompression { private readonly ICompressionProvider[] _providers; private readonly HashSet _mimeTypes; + private readonly HashSet _excludedMimeTypes; private readonly bool _enableForHttps; /// @@ -34,7 +35,9 @@ namespace Microsoft.AspNetCore.ResponseCompression throw new ArgumentNullException(nameof(options)); } - _providers = options.Value.Providers.ToArray(); + var responseCompressionOptions = options.Value; + + _providers = responseCompressionOptions.Providers.ToArray(); if (_providers.Length == 0) { // Use the factory so it can resolve IOptions from DI. @@ -59,14 +62,19 @@ namespace Microsoft.AspNetCore.ResponseCompression } } - var mimeTypes = options.Value.MimeTypes; + var mimeTypes = responseCompressionOptions.MimeTypes; if (mimeTypes == null || !mimeTypes.Any()) { mimeTypes = ResponseCompressionDefaults.MimeTypes; } _mimeTypes = new HashSet(mimeTypes, StringComparer.OrdinalIgnoreCase); - _enableForHttps = options.Value.EnableForHttps; + _excludedMimeTypes = new HashSet( + responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty(), + StringComparer.OrdinalIgnoreCase + ); + + _enableForHttps = responseCompressionOptions.EnableForHttps; } /// @@ -115,7 +123,7 @@ namespace Microsoft.AspNetCore.ResponseCompression for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; - + // Any provider is a candidate. candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } @@ -175,8 +183,9 @@ namespace Microsoft.AspNetCore.ResponseCompression mimeType = mimeType.Trim(); } - // TODO PERF: StringSegments? - return _mimeTypes.Contains(mimeType); + return ShouldCompressExact(mimeType) //check exact match type/subtype + ?? ShouldCompressPartial(mimeType) //check partial match type/* + ?? _mimeTypes.Contains("*/*"); //check wildcard */* } /// @@ -189,6 +198,35 @@ namespace Microsoft.AspNetCore.ResponseCompression return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]); } + private bool? ShouldCompressExact(string mimeType) + { + //Check excluded MIME types first, then included + if (_excludedMimeTypes.Contains(mimeType)) + { + return false; + } + + if (_mimeTypes.Contains(mimeType)) + { + return true; + } + + return null; + } + + private bool? ShouldCompressPartial(string mimeType) + { + int? slashPos = mimeType?.IndexOf('/'); + + if (slashPos >= 0) + { + string partialMimeType = mimeType.Substring(0, slashPos.Value) + "/*"; + return ShouldCompressExact(partialMimeType); + } + + return null; + } + private readonly struct ProviderCandidate : IEquatable { public ProviderCandidate(string encodingName, double quality, int priority, ICompressionProvider provider) diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs index 7340aeeac7..08fa0b283e 100644 --- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs @@ -224,6 +224,153 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); } + [Theory] + [InlineData(null, null, "text/plain", true)] + [InlineData(null, new string[0], "text/plain", true)] + [InlineData(null, new[] { "TEXT/plain" }, "text/plain", false)] + [InlineData(null, new[] { "TEXT/*" }, "text/plain", true)] + [InlineData(null, new[] { "*/*" }, "text/plain", true)] + + [InlineData(new string[0], null, "text/plain", true)] + [InlineData(new string[0], new string[0], "text/plain", true)] + [InlineData(new string[0], new[] { "TEXT/plain" }, "text/plain", false)] + [InlineData(new string[0], new[] { "TEXT/*" }, "text/plain", true)] + [InlineData(new string[0], new[] { "*/*" }, "text/plain", true)] + + [InlineData(new[] { "TEXT/plain" }, null, "text/plain", true)] + [InlineData(new[] { "TEXT/plain" }, new string[0], "text/plain", true)] + [InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/plain" }, "text/plain", false)] + [InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/*" }, "text/plain", true)] + [InlineData(new[] { "TEXT/plain" }, new[] { "*/*" }, "text/plain", true)] + + [InlineData(new[] { "TEXT/*" }, null, "text/plain", true)] + [InlineData(new[] { "TEXT/*" }, new string[0], "text/plain", true)] + [InlineData(new[] { "TEXT/*" }, new[] { "TEXT/plain" }, "text/plain", false)] + [InlineData(new[] { "TEXT/*" }, new[] { "TEXT/*" }, "text/plain", false)] + [InlineData(new[] { "TEXT/*" }, new[] { "*/*" }, "text/plain", true)] + + [InlineData(new[] { "*/*" }, null, "text/plain", true)] + [InlineData(new[] { "*/*" }, new string[0], "text/plain", true)] + [InlineData(new[] { "*/*" }, new[] { "TEXT/plain" }, "text/plain", false)] + [InlineData(new[] { "*/*" }, new[] { "TEXT/*" }, "text/plain", false)] + [InlineData(new[] { "*/*" }, new[] { "*/*" }, "text/plain", true)] + + [InlineData(null, null, "text/plain2", false)] + [InlineData(null, new string[0], "text/plain2", false)] + [InlineData(null, new[] { "TEXT/plain" }, "text/plain2", false)] + [InlineData(null, new[] { "TEXT/*" }, "text/plain2", false)] + [InlineData(null, new[] { "*/*" }, "text/plain2", false)] + + [InlineData(new string[0], null, "text/plain2", false)] + [InlineData(new string[0], new string[0], "text/plain2", false)] + [InlineData(new string[0], new[] { "TEXT/plain" }, "text/plain2", false)] + [InlineData(new string[0], new[] { "TEXT/*" }, "text/plain2", false)] + [InlineData(new string[0], new[] { "*/*" }, "text/plain2", false)] + + [InlineData(new[] { "TEXT/plain" }, null, "text/plain2", false)] + [InlineData(new[] { "TEXT/plain" }, new string[0], "text/plain2", false)] + [InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/plain" }, "text/plain2", false)] + [InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/*" }, "text/plain2", false)] + [InlineData(new[] { "TEXT/plain" }, new[] { "*/*" }, "text/plain2", false)] + + [InlineData(new[] { "TEXT/*" }, null, "text/plain2", true)] + [InlineData(new[] { "TEXT/*" }, new string[0], "text/plain2", true)] + [InlineData(new[] { "TEXT/*" }, new[] { "TEXT/plain" }, "text/plain2", true)] + [InlineData(new[] { "TEXT/*" }, new[] { "TEXT/*" }, "text/plain2", false)] + [InlineData(new[] { "TEXT/*" }, new[] { "*/*" }, "text/plain2", true)] + + [InlineData(new[] { "*/*" }, null, "text/plain2", true)] + [InlineData(new[] { "*/*" }, new string[0], "text/plain2", true)] + [InlineData(new[] { "*/*" }, new[] { "TEXT/plain" }, "text/plain2", true)] + [InlineData(new[] { "*/*" }, new[] { "TEXT/*" }, "text/plain2", false)] + [InlineData(new[] { "*/*" }, new[] { "*/*" }, "text/plain2", true)] + public async Task MimeTypes_IncludedAndExcluded( + string[] mimeTypes, + string[] excludedMimeTypes, + string mimeType, + bool compress + ) + { + var builder = new WebHostBuilder() + .ConfigureServices( + services => + services.AddResponseCompression( + options => + { + options.MimeTypes = mimeTypes; + options.ExcludedMimeTypes = excludedMimeTypes; + } + ) + ) + .Configure( + app => + { + app.UseResponseCompression(); + app.Run( + context => + { + context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; + context.Response.ContentType = mimeType; + return context.Response.WriteAsync(new string('a', 100)); + } + ); + } + ); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + request.Headers.AcceptEncoding.ParseAdd("gzip"); + + var response = await client.SendAsync(request); + + if (compress) + { + CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + } + else + { + CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false); + } + } + + [Fact] + public async Task NoIncludedMimeTypes_UseDefaults() + { + var builder = new WebHostBuilder() + .ConfigureServices( + services => + services.AddResponseCompression( + options => options.ExcludedMimeTypes = new[] { "text/*" } + ) + ) + .Configure( + app => + { + app.UseResponseCompression(); + app.Run( + context => + { + context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; + context.Response.ContentType = TextPlain; + return context.Response.WriteAsync(new string('a', 100)); + } + ); + } + ); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + request.Headers.AcceptEncoding.ParseAdd("gzip"); + + var response = await client.SendAsync(request); + + CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip"); + } + [Theory] [InlineData("")] [InlineData("text/plain")]