Support mime-type wildcards #121 @andrewslavin

This commit is contained in:
andrewslavin 2018-03-17 23:59:54 +11:00 коммит произвёл Chris Ross (ASP.NET)
Родитель e0be511964
Коммит cc610955db
4 изменённых файлов: 202 добавлений и 7 удалений

Просмотреть файл

@ -26,6 +26,11 @@ namespace ResponseCompressionSample
options.Providers.Add<CustomCompressionProvider>();
// .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/*" };
});
}

Просмотреть файл

@ -15,9 +15,14 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// </summary>
public IEnumerable<string> MimeTypes { get; set; }
/// <summary>
/// Response Content-Type MIME types to not compress.
/// </summary>
public IEnumerable<string> ExcludedMimeTypes { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool EnableForHttps { get; set; } = false;

Просмотреть файл

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
{
private readonly ICompressionProvider[] _providers;
private readonly HashSet<string> _mimeTypes;
private readonly HashSet<string> _excludedMimeTypes;
private readonly bool _enableForHttps;
/// <summary>
@ -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<GzipCompressionProviderOptions> 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<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
_enableForHttps = options.Value.EnableForHttps;
_excludedMimeTypes = new HashSet<string>(
responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty<string>(),
StringComparer.OrdinalIgnoreCase
);
_enableForHttps = responseCompressionOptions.EnableForHttps;
}
/// <inheritdoc />
@ -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 */*
}
/// <inheritdoc />
@ -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<ProviderCandidate>
{
public ProviderCandidate(string encodingName, double quality, int priority, ICompressionProvider provider)

Просмотреть файл

@ -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")]