Support mime-type wildcards #121 @andrewslavin
This commit is contained in:
Родитель
e0be511964
Коммит
cc610955db
|
@ -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")]
|
||||
|
|
Загрузка…
Ссылка в новой задаче