Merge release/2.2 from aspnet/BasicMiddleware
This commit is contained in:
commit
7e823a05ec
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,11 @@ namespace Microsoft.AspNetCore.HostFiltering
|
||||||
throw new InvalidOperationException("No allowed hosts were configured.");
|
throw new InvalidOperationException("No allowed hosts were configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Allowed hosts: " + string.Join("; ", allowedHosts));
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Allowed hosts: {Hosts}", string.Join("; ", allowedHosts));
|
||||||
|
}
|
||||||
|
|
||||||
_allowedHosts = allowedHosts;
|
_allowedHosts = allowedHosts;
|
||||||
return _allowedHosts;
|
return _allowedHosts;
|
||||||
}
|
}
|
||||||
|
|
@ -148,29 +152,26 @@ namespace Microsoft.AspNetCore.HostFiltering
|
||||||
// Http/1.1 requires the header but the value may be empty.
|
// Http/1.1 requires the header but the value may be empty.
|
||||||
if (!_options.AllowEmptyHosts)
|
if (!_options.AllowEmptyHosts)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"{context.Request.Protocol} request rejected due to missing or empty host header.");
|
_logger.LogInformation("{Protocol} request rejected due to missing or empty host header.", context.Request.Protocol);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
_logger.LogDebug("{Protocol} request allowed with missing or empty host header.", context.Request.Protocol);
|
||||||
{
|
|
||||||
_logger.LogDebug($"{context.Request.Protocol} request allowed with missing or empty host header.");
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_allowAnyNonEmptyHost == true)
|
if (_allowAnyNonEmptyHost == true)
|
||||||
{
|
{
|
||||||
_logger.LogTrace($"All hosts are allowed.");
|
_logger.LogTrace("All hosts are allowed.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HostString.MatchesAny(host, allowedHosts))
|
if (HostString.MatchesAny(host, allowedHosts))
|
||||||
{
|
{
|
||||||
_logger.LogTrace($"The host '{host}' matches an allowed host.");
|
_logger.LogTrace("The host '{Host}' matches an allowed host.", host);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation($"The host '{host}' does not match an allowed host.");
|
_logger.LogInformation("The host '{Host}' does not match an allowed host.", host);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
||||||
if (currentValues.RemoteIpAndPort != null && checkKnownIps && !CheckKnownAddress(currentValues.RemoteIpAndPort.Address))
|
if (currentValues.RemoteIpAndPort != null && checkKnownIps && !CheckKnownAddress(currentValues.RemoteIpAndPort.Address))
|
||||||
{
|
{
|
||||||
// Stop at the first unknown remote IP, but still apply changes processed so far.
|
// Stop at the first unknown remote IP, but still apply changes processed so far.
|
||||||
_logger.LogDebug(1, $"Unknown proxy: {currentValues.RemoteIpAndPort}");
|
_logger.LogDebug(1, "Unknown proxy: {RemoteIpAndPort}", currentValues.RemoteIpAndPort);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,12 +248,12 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
||||||
else if (!string.IsNullOrEmpty(set.IpAndPortText))
|
else if (!string.IsNullOrEmpty(set.IpAndPortText))
|
||||||
{
|
{
|
||||||
// Stop at the first unparsable IP, but still apply changes processed so far.
|
// Stop at the first unparsable IP, but still apply changes processed so far.
|
||||||
_logger.LogDebug(1, $"Unparsable IP: {set.IpAndPortText}");
|
_logger.LogDebug(1, "Unparsable IP: {IpAndPortText}", set.IpAndPortText);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else if (_options.RequireHeaderSymmetry)
|
else if (_options.RequireHeaderSymmetry)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(2, $"Missing forwarded IPAddress.");
|
_logger.LogWarning(2, "Missing forwarded IPAddress.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,6 +349,14 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
||||||
|
|
||||||
private bool CheckKnownAddress(IPAddress address)
|
private bool CheckKnownAddress(IPAddress address)
|
||||||
{
|
{
|
||||||
|
if (address.IsIPv4MappedToIPv6)
|
||||||
|
{
|
||||||
|
var ipv4Address = address.MapToIPv4();
|
||||||
|
if (CheckKnownAddress(ipv4Address))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (_options.KnownProxies.Contains(address))
|
if (_options.KnownProxies.Contains(address))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.HttpOverrides.Internal
|
namespace Microsoft.AspNetCore.HttpOverrides.Internal
|
||||||
|
|
@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.HttpOverrides.Internal
|
||||||
if (portPart != null)
|
if (portPart != null)
|
||||||
{
|
{
|
||||||
int port;
|
int port;
|
||||||
if (int.TryParse(portPart, out port))
|
if (int.TryParse(portPart, NumberStyles.None, CultureInfo.InvariantCulture, out port))
|
||||||
{
|
{
|
||||||
endpoint = new IPEndPoint(address, port);
|
endpoint = new IPEndPoint(address, port);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -822,5 +822,48 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
||||||
Assert.Equal("localhost", context.Request.Host.ToString());
|
Assert.Equal("localhost", context.Request.Host.ToString());
|
||||||
Assert.Equal("Protocol", context.Request.Scheme);
|
Assert.Equal("Protocol", context.Request.Scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("22.33.44.55,::ffff:127.0.0.1", "", "", "22.33.44.55")]
|
||||||
|
[InlineData("22.33.44.55,::ffff:172.123.142.121", "172.123.142.121", "", "22.33.44.55")]
|
||||||
|
[InlineData("22.33.44.55,::ffff:172.123.142.121", "::ffff:172.123.142.121", "", "22.33.44.55")]
|
||||||
|
[InlineData("22.33.44.55,::ffff:172.123.142.121,172.32.24.23", "", "172.0.0.0/8", "22.33.44.55")]
|
||||||
|
[InlineData("2a00:1450:4009:802::200e,2a02:26f0:2d:183::356e,::ffff:172.123.142.121,172.32.24.23", "", "172.0.0.0/8,2a02:26f0:2d:183::1/64", "2a00:1450:4009:802::200e")]
|
||||||
|
[InlineData("22.33.44.55,2a02:26f0:2d:183::356e,::ffff:127.0.0.1", "2a02:26f0:2d:183::356e", "", "22.33.44.55")]
|
||||||
|
public async Task XForwardForIPv4ToIPv6Mapping(string forHeader, string knownProxies, string knownNetworks, string expectedRemoteIp)
|
||||||
|
{
|
||||||
|
var options = new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor,
|
||||||
|
ForwardLimit = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var knownProxy in knownProxies.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var proxy = IPAddress.Parse(knownProxy);
|
||||||
|
options.KnownProxies.Add(proxy);
|
||||||
|
}
|
||||||
|
foreach (var knownNetwork in knownNetworks.Split(new string[] { "," }, options:StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var knownNetworkParts = knownNetwork.Split('/');
|
||||||
|
var networkIp = IPAddress.Parse(knownNetworkParts[0]);
|
||||||
|
var prefixLength = int.Parse(knownNetworkParts[1]);
|
||||||
|
options.KnownNetworks.Add(new IPNetwork(networkIp, prefixLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseForwardedHeaders(options);
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
|
var context = await server.SendAsync(c =>
|
||||||
|
{
|
||||||
|
c.Request.Headers["X-Forwarded-For"] = forHeader;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(expectedRemoteIp, context.Connection.RemoteIpAddress.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.HttpsPolicy.Internal;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
@ -24,13 +27,15 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly StringValues _strictTransportSecurityValue;
|
private readonly StringValues _strictTransportSecurityValue;
|
||||||
private readonly IList<string> _excludedHosts;
|
private readonly IList<string> _excludedHosts;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the HSTS middleware.
|
/// Initialize the HSTS middleware.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next"></param>
|
/// <param name="next"></param>
|
||||||
/// <param name="options"></param>
|
/// <param name="options"></param>
|
||||||
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options)
|
/// <param name="loggerFactory"></param>
|
||||||
|
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
if (options == null)
|
if (options == null)
|
||||||
{
|
{
|
||||||
|
|
@ -46,8 +51,17 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
var preload = hstsOptions.Preload ? Preload : StringSegment.Empty;
|
var preload = hstsOptions.Preload ? Preload : StringSegment.Empty;
|
||||||
_strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}");
|
_strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}");
|
||||||
_excludedHosts = hstsOptions.ExcludedHosts;
|
_excludedHosts = hstsOptions.ExcludedHosts;
|
||||||
|
_logger = loggerFactory.CreateLogger<HstsMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the HSTS middleware.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options)
|
||||||
|
: this(next, options, NullLoggerFactory.Instance) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoke the middleware.
|
/// Invoke the middleware.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -55,11 +69,21 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task Invoke(HttpContext context)
|
public Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Request.IsHttps && !IsHostExcluded(context.Request.Host.Host))
|
if (!context.Request.IsHttps)
|
||||||
{
|
{
|
||||||
context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue;
|
_logger.SkippingInsecure();
|
||||||
|
return _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsHostExcluded(context.Request.Host.Host))
|
||||||
|
{
|
||||||
|
_logger.SkippingExcludedHost(context.Request.Host.Host);
|
||||||
|
return _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue;
|
||||||
|
_logger.AddingHstsHeader();
|
||||||
|
|
||||||
return _next(context);
|
return _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy
|
||||||
/// If the HttpsPort is not set, we will try to get the HttpsPort from the following:
|
/// If the HttpsPort is not set, we will try to get the HttpsPort from the following:
|
||||||
/// 1. HTTPS_PORT environment variable
|
/// 1. HTTPS_PORT environment variable
|
||||||
/// 2. IServerAddressesFeature
|
/// 2. IServerAddressesFeature
|
||||||
/// 3. 443 (or not set)
|
/// If that fails then the middleware will log a warning and turn off.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public int? HttpsPort { get; set; }
|
public int? HttpsPort { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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 Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.HttpsPolicy.Internal
|
||||||
|
{
|
||||||
|
internal static class HstsLoggingExtensions
|
||||||
|
{
|
||||||
|
private static readonly Action<ILogger, Exception> _notSecure;
|
||||||
|
private static readonly Action<ILogger, string, Exception> _excludedHost;
|
||||||
|
private static readonly Action<ILogger, Exception> _addingHstsHeader;
|
||||||
|
|
||||||
|
static HstsLoggingExtensions()
|
||||||
|
{
|
||||||
|
_notSecure = LoggerMessage.Define(LogLevel.Debug, 1, "The request is insecure. Skipping HSTS header.");
|
||||||
|
_excludedHost = LoggerMessage.Define<string>(LogLevel.Debug, 2, "The host '{host}' is excluded. Skipping HSTS header.");
|
||||||
|
_addingHstsHeader = LoggerMessage.Define(LogLevel.Trace, 3, "Adding HSTS header to response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SkippingInsecure(this ILogger logger)
|
||||||
|
{
|
||||||
|
_notSecure(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SkippingExcludedHost(this ILogger logger, string host)
|
||||||
|
{
|
||||||
|
_excludedHost(logger, host, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddingHstsHeader(this ILogger logger)
|
||||||
|
{
|
||||||
|
_addingHstsHeader(logger, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -131,7 +133,16 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
[InlineData("[::1]")]
|
[InlineData("[::1]")]
|
||||||
public async Task DefaultExcludesCommonLocalhostDomains_DoesNotSetHstsHeader(string host)
|
public async Task DefaultExcludesCommonLocalhostDomains_DoesNotSetHstsHeader(string host)
|
||||||
{
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>,
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
|
})
|
||||||
.Configure(app =>
|
.Configure(app =>
|
||||||
{
|
{
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
|
|
@ -149,6 +160,13 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Empty(response.Headers);
|
Assert.Empty(response.Headers);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
|
||||||
|
Assert.Single(logMessages);
|
||||||
|
var message = logMessages.Single();
|
||||||
|
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||||
|
Assert.Equal($"The host '{host}' is excluded. Skipping HSTS header.", message.State.ToString(), ignoreCase: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -157,9 +175,16 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
[InlineData("[::1]")]
|
[InlineData("[::1]")]
|
||||||
public async Task AllowLocalhostDomainsIfListIsReset_SetHstsHeader(string host)
|
public async Task AllowLocalhostDomainsIfListIsReset_SetHstsHeader(string host)
|
||||||
{
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>,
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
|
|
||||||
services.AddHsts(options =>
|
services.AddHsts(options =>
|
||||||
{
|
{
|
||||||
options.ExcludedHosts.Clear();
|
options.ExcludedHosts.Clear();
|
||||||
|
|
@ -182,6 +207,13 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Single(response.Headers);
|
Assert.Single(response.Headers);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
|
||||||
|
Assert.Single(logMessages);
|
||||||
|
var message = logMessages.Single();
|
||||||
|
Assert.Equal(LogLevel.Trace, message.LogLevel);
|
||||||
|
Assert.Equal("Adding HSTS header to response.", message.State.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -190,9 +222,16 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
[InlineData("EXAMPLE.COM")]
|
[InlineData("EXAMPLE.COM")]
|
||||||
public async Task AddExcludedDomains_DoesNotAddHstsHeader(string host)
|
public async Task AddExcludedDomains_DoesNotAddHstsHeader(string host)
|
||||||
{
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>,
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
|
|
||||||
services.AddHsts(options => {
|
services.AddHsts(options => {
|
||||||
options.ExcludedHosts.Add(host);
|
options.ExcludedHosts.Add(host);
|
||||||
});
|
});
|
||||||
|
|
@ -214,6 +253,91 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Empty(response.Headers);
|
Assert.Empty(response.Headers);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
|
||||||
|
Assert.Single(logMessages);
|
||||||
|
var message = logMessages.Single();
|
||||||
|
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||||
|
Assert.Equal($"The host '{host}' is excluded. Skipping HSTS header.", message.State.ToString(), ignoreCase: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WhenRequestIsInsecure_DoesNotAddHstsHeader()
|
||||||
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>,
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri("http://example.com:5050");
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Empty(response.Headers);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
|
||||||
|
Assert.Single(logMessages);
|
||||||
|
var message = logMessages.Single();
|
||||||
|
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||||
|
Assert.Equal("The request is insecure. Skipping HSTS header.", message.State.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WhenRequestIsSecure_AddsHstsHeader()
|
||||||
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>,
|
||||||
|
TestSink.EnableWithTypeName<HstsMiddleware>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
return context.Response.WriteAsync("Hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri("https://example.com:5050");
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Contains(response.Headers, x => x.Key == HeaderNames.StrictTransportSecurity);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
|
||||||
|
Assert.Single(logMessages);
|
||||||
|
var message = logMessages.Single();
|
||||||
|
Assert.Equal(LogLevel.Trace, message.LogLevel);
|
||||||
|
Assert.Equal("Adding HSTS header to response.", message.State.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
|
||||||
<TargetFrameworks Condition=" '$(DeveloperBuild)' != 'true' ">$(TargetFrameworks);netcoreapp2.0</TargetFrameworks>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="BenchmarkDotNet" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" PrivateAssets="All" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
|
||||||
|
<Reference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCompression.Benchmarks
|
||||||
|
{
|
||||||
|
public class ResponseCompressionProviderBenchmark
|
||||||
|
{
|
||||||
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddOptions()
|
||||||
|
.AddResponseCompression()
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
var options = new ResponseCompressionOptions();
|
||||||
|
|
||||||
|
Provider = new ResponseCompressionProvider(services, Options.Create(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
[ParamsSource(nameof(EncodingStrings))]
|
||||||
|
public string AcceptEncoding { get; set; }
|
||||||
|
|
||||||
|
public static IEnumerable<string> EncodingStrings()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"gzip;q=0.8, compress;q=0.6, br;q=0.4",
|
||||||
|
"gzip, compress, br",
|
||||||
|
"br, compress, gzip",
|
||||||
|
"gzip, compress",
|
||||||
|
"identity",
|
||||||
|
"*"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseCompressionProvider Provider { get; set; }
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public ICompressionProvider GetCompressionProvider()
|
||||||
|
{
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
|
||||||
|
context.Request.Headers[HeaderNames.AcceptEncoding] = AcceptEncoding;
|
||||||
|
|
||||||
|
return Provider.GetCompressionProvider(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ namespace ResponseCompressionSample
|
||||||
options.Providers.Add<CustomCompressionProvider>();
|
options.Providers.Add<CustomCompressionProvider>();
|
||||||
// .Append(TItem) is only available on Core.
|
// .Append(TItem) is only available on Core.
|
||||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
|
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/*" };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Brotli compression provider.
|
||||||
|
/// </summary>
|
||||||
|
public class BrotliCompressionProvider : ICompressionProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="BrotliCompressionProvider"/> with options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
public BrotliCompressionProvider(IOptions<BrotliCompressionProviderOptions> options)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
Options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BrotliCompressionProviderOptions Options { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string EncodingName => "br";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool SupportsFlush => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Stream CreateStream(Stream outputStream)
|
||||||
|
{
|
||||||
|
#if NETCOREAPP2_1
|
||||||
|
return new BrotliStream(outputStream, Options.Level, leaveOpen: true);
|
||||||
|
#elif NET461 || NETSTANDARD2_0
|
||||||
|
// Brotli is only supported in .NET Core 2.1+
|
||||||
|
throw new PlatformNotSupportedException();
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// 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.IO.Compression;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for the <see cref="BrotliCompressionProvider"/>
|
||||||
|
/// </summary>
|
||||||
|
public class BrotliCompressionProviderOptions : IOptions<BrotliCompressionProviderOptions>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// What level of compression to use for the stream. The default is <see cref="CompressionLevel.Fastest"/>.
|
||||||
|
/// </summary>
|
||||||
|
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
BrotliCompressionProviderOptions IOptions<BrotliCompressionProviderOptions>.Value => this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
#if NET461
|
#if NET461
|
||||||
return false;
|
return false;
|
||||||
#elif NETSTANDARD2_0
|
#elif NETSTANDARD2_0 || NETCOREAPP2_1
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
#error target frameworks need to be updated
|
#error target frameworks need to be updated
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,20 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>ASP.NET Core middleware for HTTP Response compression.</Description>
|
<Description>ASP.NET Core middleware for HTTP Response compression.</Description>
|
||||||
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
|
<TargetFrameworks>net461;netstandard2.0;netcoreapp2.1</TargetFrameworks>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<PackageTags>aspnetcore</PackageTags>
|
<PackageTags>aspnetcore</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<<<<<<< HEAD:src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||||
|
=======
|
||||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||||
<Reference Include="Microsoft.Extensions.Options" />
|
<Reference Include="Microsoft.Extensions.Options" />
|
||||||
|
>>>>>>> m21:src/Middleware/ResponseCompression/src/Microsoft.AspNetCore.ResponseCompression.csproj
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
"text/xml",
|
"text/xml",
|
||||||
"application/json",
|
"application/json",
|
||||||
"text/json",
|
"text/json",
|
||||||
|
// WebAssembly
|
||||||
|
"application/wasm",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,20 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> MimeTypes { get; set; }
|
public IEnumerable<string> MimeTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response Content-Type MIME types to not compress.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<string> ExcludedMimeTypes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if responses over HTTPS connections should be compressed. The default is 'false'.
|
/// 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>
|
/// </summary>
|
||||||
public bool EnableForHttps { get; set; } = false;
|
public bool EnableForHttps { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ICompressionProviders to use for responses.
|
/// The <see cref="ICompressionProvider"/> types to use for responses.
|
||||||
|
/// Providers are prioritized based on the order they are added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection();
|
public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
@ -16,7 +20,9 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
private readonly ICompressionProvider[] _providers;
|
private readonly ICompressionProvider[] _providers;
|
||||||
private readonly HashSet<string> _mimeTypes;
|
private readonly HashSet<string> _mimeTypes;
|
||||||
|
private readonly HashSet<string> _excludedMimeTypes;
|
||||||
private readonly bool _enableForHttps;
|
private readonly bool _enableForHttps;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If no compression providers are specified then GZip is used by default.
|
/// If no compression providers are specified then GZip is used by default.
|
||||||
|
|
@ -34,11 +40,23 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
_providers = options.Value.Providers.ToArray();
|
var responseCompressionOptions = options.Value;
|
||||||
|
|
||||||
|
_providers = responseCompressionOptions.Providers.ToArray();
|
||||||
if (_providers.Length == 0)
|
if (_providers.Length == 0)
|
||||||
{
|
{
|
||||||
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI.
|
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI.
|
||||||
_providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(GzipCompressionProvider)) };
|
_providers = new ICompressionProvider[]
|
||||||
|
{
|
||||||
|
#if NETCOREAPP2_1
|
||||||
|
new CompressionProviderFactory(typeof(BrotliCompressionProvider)),
|
||||||
|
#elif NET461 || NETSTANDARD2_0
|
||||||
|
// Brotli is only supported in .NET Core 2.1+
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
new CompressionProviderFactory(typeof(GzipCompressionProvider)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
for (var i = 0; i < _providers.Length; i++)
|
for (var i = 0; i < _providers.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -49,58 +67,110 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mimeTypes = options.Value.MimeTypes;
|
var mimeTypes = responseCompressionOptions.MimeTypes;
|
||||||
if (mimeTypes == null || !mimeTypes.Any())
|
if (mimeTypes == null || !mimeTypes.Any())
|
||||||
{
|
{
|
||||||
mimeTypes = ResponseCompressionDefaults.MimeTypes;
|
mimeTypes = ResponseCompressionDefaults.MimeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
|
_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;
|
||||||
|
|
||||||
|
_logger = services.GetRequiredService<ILogger<ResponseCompressionProvider>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
|
public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
|
||||||
{
|
{
|
||||||
IList<StringWithQualityHeaderValue> unsorted;
|
|
||||||
|
|
||||||
// e.g. Accept-Encoding: gzip, deflate, sdch
|
// e.g. Accept-Encoding: gzip, deflate, sdch
|
||||||
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
|
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
|
||||||
if (!StringValues.IsNullOrEmpty(accept)
|
|
||||||
&& StringWithQualityHeaderValue.TryParseList(accept, out unsorted)
|
// Note this is already checked in CheckRequestAcceptsCompression which _should_ prevent any of these other methods from being called.
|
||||||
&& unsorted != null && unsorted.Count > 0)
|
if (StringValues.IsNullOrEmpty(accept))
|
||||||
{
|
{
|
||||||
// TODO PERF: clients don't usually include quality values so this sort will not have any effect. Fast-path?
|
Debug.Assert(false, "Duplicate check failed.");
|
||||||
var sorted = unsorted
|
_logger.NoAcceptEncoding();
|
||||||
.Where(s => s.Quality.GetValueOrDefault(1) > 0)
|
return null;
|
||||||
.OrderByDescending(s => s.Quality.GetValueOrDefault(1));
|
}
|
||||||
|
|
||||||
foreach (var encoding in sorted)
|
if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || !encodings.Any())
|
||||||
|
{
|
||||||
|
_logger.NoAcceptEncoding();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = new HashSet<ProviderCandidate>();
|
||||||
|
|
||||||
|
foreach (var encoding in encodings)
|
||||||
|
{
|
||||||
|
var encodingName = encoding.Value;
|
||||||
|
var quality = encoding.Quality.GetValueOrDefault(1);
|
||||||
|
|
||||||
|
if (quality < double.Epsilon)
|
||||||
{
|
{
|
||||||
// There will rarely be more than three providers, and there's only one by default
|
continue;
|
||||||
foreach (var provider in _providers)
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _providers.Length; i++)
|
||||||
|
{
|
||||||
|
var provider = _providers[i];
|
||||||
|
|
||||||
|
if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (StringSegment.Equals(provider.EncodingName, encoding.Value, StringComparison.OrdinalIgnoreCase))
|
candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider));
|
||||||
{
|
}
|
||||||
return provider;
|
}
|
||||||
}
|
|
||||||
|
// Uncommon but valid options
|
||||||
|
if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncommon but valid options
|
break;
|
||||||
if (StringSegment.Equals("*", encoding.Value, StringComparison.Ordinal))
|
}
|
||||||
{
|
|
||||||
// Any
|
if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase))
|
||||||
return _providers[0];
|
{
|
||||||
}
|
// We add 'identity' to the list of "candidates" with a very low priority and no provider.
|
||||||
if (StringSegment.Equals("identity", encoding.Value, StringComparison.OrdinalIgnoreCase))
|
// This will allow it to be ordered based on its quality (and priority) later in the method.
|
||||||
{
|
candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null));
|
||||||
// No compression
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
ICompressionProvider selectedProvider = null;
|
||||||
|
if (candidates.Count <= 1)
|
||||||
|
{
|
||||||
|
selectedProvider = candidates.FirstOrDefault().Provider;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectedProvider = candidates
|
||||||
|
.OrderByDescending(x => x.Quality)
|
||||||
|
.ThenBy(x => x.Priority)
|
||||||
|
.First().Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProvider == null)
|
||||||
|
{
|
||||||
|
// "identity" would match as a candidate but not have a provider implementation
|
||||||
|
_logger.NoCompressionProvider();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CompressingWith(selectedProvider.EncodingName);
|
||||||
|
return selectedProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -108,6 +178,13 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
|
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
|
||||||
{
|
{
|
||||||
|
_logger.NoCompressionDueToHeader(HeaderNames.ContentRange);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding))
|
||||||
|
{
|
||||||
|
_logger.NoCompressionDueToHeader(HeaderNames.ContentEncoding);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(mimeType))
|
if (string.IsNullOrEmpty(mimeType))
|
||||||
{
|
{
|
||||||
|
_logger.NoCompressionForContentType(mimeType);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,8 +204,18 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
mimeType = mimeType.Trim();
|
mimeType = mimeType.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO PERF: StringSegments?
|
var shouldCompress = ShouldCompressExact(mimeType) //check exact match type/subtype
|
||||||
return _mimeTypes.Contains(mimeType);
|
?? ShouldCompressPartial(mimeType) //check partial match type/*
|
||||||
|
?? _mimeTypes.Contains("*/*"); //check wildcard */*
|
||||||
|
|
||||||
|
if (shouldCompress)
|
||||||
|
{
|
||||||
|
_logger.ShouldCompressResponse(); // Trace, there will be more logs
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.NoCompressionForContentType(mimeType);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -135,9 +223,81 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
if (context.Request.IsHttps && !_enableForHttps)
|
if (context.Request.IsHttps && !_enableForHttps)
|
||||||
{
|
{
|
||||||
|
_logger.NoCompressionForHttps();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
|
|
||||||
|
if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
|
||||||
|
{
|
||||||
|
_logger.NoAcceptEncoding();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.RequestAcceptsCompression(); // Trace, there will be more logs
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
EncodingName = encodingName;
|
||||||
|
Quality = quality;
|
||||||
|
Priority = priority;
|
||||||
|
Provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EncodingName { get; }
|
||||||
|
|
||||||
|
public double Quality { get; }
|
||||||
|
|
||||||
|
public int Priority { get; }
|
||||||
|
|
||||||
|
public ICompressionProvider Provider { get; }
|
||||||
|
|
||||||
|
public bool Equals(ProviderCandidate other)
|
||||||
|
{
|
||||||
|
return string.Equals(EncodingName, other.EncodingName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return obj is ProviderCandidate candidate && Equals(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return StringComparer.OrdinalIgnoreCase.GetHashCode(EncodingName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// 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 Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCompression.Internal
|
||||||
|
{
|
||||||
|
internal static class ResponseCompressionLoggingExtensions
|
||||||
|
{
|
||||||
|
private static readonly Action<ILogger, Exception> _noAcceptEncoding;
|
||||||
|
private static readonly Action<ILogger, Exception> _noCompressionForHttps;
|
||||||
|
private static readonly Action<ILogger, Exception> _requestAcceptsCompression;
|
||||||
|
private static readonly Action<ILogger, string, Exception> _noCompressionDueToHeader;
|
||||||
|
private static readonly Action<ILogger, string, Exception> _noCompressionForContentType;
|
||||||
|
private static readonly Action<ILogger, Exception> _shouldCompressResponse;
|
||||||
|
private static readonly Action<ILogger, Exception> _noCompressionProvider;
|
||||||
|
private static readonly Action<ILogger, string, Exception> _compressWith;
|
||||||
|
|
||||||
|
static ResponseCompressionLoggingExtensions()
|
||||||
|
{
|
||||||
|
_noAcceptEncoding = LoggerMessage.Define(LogLevel.Debug, 1, "No response compression available, the Accept-Encoding header is missing or invalid.");
|
||||||
|
_noCompressionForHttps = LoggerMessage.Define(LogLevel.Debug, 2, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
|
||||||
|
_requestAcceptsCompression = LoggerMessage.Define(LogLevel.Trace, 3, "This request accepts compression.");
|
||||||
|
_noCompressionDueToHeader = LoggerMessage.Define<string>(LogLevel.Debug, 4, "Response compression disabled due to the {header} header.");
|
||||||
|
_noCompressionForContentType = LoggerMessage.Define<string>(LogLevel.Debug, 5, "Response compression is not enabled for the Content-Type '{header}'.");
|
||||||
|
_shouldCompressResponse = LoggerMessage.Define(LogLevel.Trace, 6, "Response compression is available for this Content-Type.");
|
||||||
|
_noCompressionProvider = LoggerMessage.Define(LogLevel.Debug, 7, "No matching response compression provider found.");
|
||||||
|
_compressWith = LoggerMessage.Define<string>(LogLevel.Debug, 8, "The response will be compressed with '{provider}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NoAcceptEncoding(this ILogger logger)
|
||||||
|
{
|
||||||
|
_noAcceptEncoding(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NoCompressionForHttps(this ILogger logger)
|
||||||
|
{
|
||||||
|
_noCompressionForHttps(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequestAcceptsCompression(this ILogger logger)
|
||||||
|
{
|
||||||
|
_requestAcceptsCompression(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NoCompressionDueToHeader(this ILogger logger, string header)
|
||||||
|
{
|
||||||
|
_noCompressionDueToHeader(logger, header, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NoCompressionForContentType(this ILogger logger, string header)
|
||||||
|
{
|
||||||
|
_noCompressionForContentType(logger, header, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ShouldCompressResponse(this ILogger logger)
|
||||||
|
{
|
||||||
|
_shouldCompressResponse(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NoCompressionProvider(this ILogger logger)
|
||||||
|
{
|
||||||
|
_noCompressionProvider(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CompressingWith(this ILogger logger, string provider)
|
||||||
|
{
|
||||||
|
_compressWith(logger, provider, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
|
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
|
||||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -14,6 +15,8 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -23,6 +26,26 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
{
|
{
|
||||||
private const string TextPlain = "text/plain";
|
private const string TextPlain = "text/plain";
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SupportedEncodings =>
|
||||||
|
TestData.Select(x => new object[] { x.EncodingName });
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SupportedEncodingsWithBodyLength =>
|
||||||
|
TestData.Select(x => new object[] { x.EncodingName, x.ExpectedBodyLength });
|
||||||
|
|
||||||
|
private static IEnumerable<EncodingTestData> TestData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new EncodingTestData("gzip", expectedBodyLength: 24);
|
||||||
|
#if NETCOREAPP2_2
|
||||||
|
yield return new EncodingTestData("br", expectedBodyLength: 20);
|
||||||
|
#elif NET461
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Options_HttpsDisabledByDefault()
|
public void Options_HttpsDisabledByDefault()
|
||||||
{
|
{
|
||||||
|
|
@ -34,25 +57,86 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Request_NoAcceptEncoding_Uncompressed()
|
public async Task Request_NoAcceptEncoding_Uncompressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||||
|
AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available, the Accept-Encoding header is missing or invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Request_AcceptGzipDeflate_CompressedGzip()
|
public async Task Request_AcceptGzipDeflate_CompressedGzip()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip", "deflate" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Request_AcceptBrotli_CompressedBrotli()
|
||||||
|
{
|
||||||
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "br" }, responseType: TextPlain);
|
||||||
|
|
||||||
|
#if NET461
|
||||||
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
||||||
|
#elif NETCOREAPP2_2
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
||||||
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("gzip", "br")]
|
||||||
|
[InlineData("br", "gzip")]
|
||||||
|
public async Task Request_AcceptMixed_CompressedBrotli(string encoding1, string encoding2)
|
||||||
|
{
|
||||||
|
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain);
|
||||||
|
|
||||||
|
#if NET461
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
#elif NETCOREAPP2_2
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
||||||
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NETCOREAPP2_2
|
||||||
|
[Theory]
|
||||||
|
[InlineData("gzip", "br")]
|
||||||
|
[InlineData("br", "gzip")]
|
||||||
|
public async Task Request_AcceptMixed_ConfiguredOrder_CompressedGzip(string encoding1, string encoding2)
|
||||||
|
{
|
||||||
|
void Configure(ResponseCompressionOptions options)
|
||||||
|
{
|
||||||
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
|
options.Providers.Add<BrotliCompressionProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (response, logMessages) = await InvokeMiddleware(100, new[] { encoding1, encoding2 }, responseType: TextPlain, configure: Configure);
|
||||||
|
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
}
|
||||||
|
#elif NET461
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Request_AcceptUnknown_NotCompressed()
|
public async Task Request_AcceptUnknown_NotCompressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "unknown" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "unknown" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
||||||
|
Assert.Equal(3, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type.");
|
||||||
|
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -62,31 +146,10 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
[InlineData("text/plain ; charset=ISO-8859-4")]
|
[InlineData("text/plain ; charset=ISO-8859-4")]
|
||||||
public async Task ContentType_WithCharset_Compress(string contentType)
|
public async Task ContentType_WithCharset_Compress(string contentType)
|
||||||
{
|
{
|
||||||
var builder = new WebHostBuilder()
|
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType);
|
||||||
.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
services.AddResponseCompression();
|
|
||||||
})
|
|
||||||
.Configure(app =>
|
|
||||||
{
|
|
||||||
app.UseResponseCompression();
|
|
||||||
app.Run(context =>
|
|
||||||
{
|
|
||||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
|
||||||
context.Response.ContentType = contentType;
|
|
||||||
return context.Response.WriteAsync(new string('a', 100));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var server = new TestServer(builder);
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
var client = server.CreateClient();
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -117,7 +180,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 123);
|
CheckResponseCompressed(response, expectedBodyLength: 123, expectedEncoding: "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -125,31 +188,114 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
[InlineData("text/plain2")]
|
[InlineData("text/plain2")]
|
||||||
public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType)
|
public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType)
|
||||||
{
|
{
|
||||||
var builder = new WebHostBuilder()
|
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType);
|
||||||
.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
services.AddResponseCompression();
|
|
||||||
})
|
|
||||||
.Configure(app =>
|
|
||||||
{
|
|
||||||
app.UseResponseCompression();
|
|
||||||
app.Run(context =>
|
|
||||||
{
|
|
||||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
|
||||||
context.Response.ContentType = contentType;
|
|
||||||
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);
|
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||||
|
Assert.Equal(2, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
var expected = string.IsNullOrEmpty(contentType) ? "(null)" : contentType;
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Response compression is not enabled for the Content-Type '{expected}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 contentType,
|
||||||
|
bool compress
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, contentType,
|
||||||
|
configure: options =>
|
||||||
|
{
|
||||||
|
options.MimeTypes = mimeTypes;
|
||||||
|
options.ExcludedMimeTypes = excludedMimeTypes;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (compress)
|
||||||
|
{
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||||
|
Assert.Equal(2, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, $"Response compression is not enabled for the Content-Type '{contentType}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NoIncludedMimeTypes_UseDefaults()
|
||||||
|
{
|
||||||
|
var (response, logMessages) = await InvokeMiddleware(uncompressedBodyLength: 100, requestAcceptEncodings: new[] { "gzip" }, TextPlain,
|
||||||
|
configure: options =>
|
||||||
|
{
|
||||||
|
options.ExcludedMimeTypes = new[] { "text/*" };
|
||||||
|
});
|
||||||
|
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -191,71 +337,98 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Request_AcceptStar_Compressed()
|
public async Task Request_AcceptStar_Compressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "*" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "*" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
#if NET461
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
#elif NETCOREAPP2_2
|
||||||
|
CheckResponseCompressed(response, expectedBodyLength: 20, expectedEncoding: "br");
|
||||||
|
AssertCompressedWithLog(logMessages, "br");
|
||||||
|
#else
|
||||||
|
#error Target frameworks need to be updated.
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Request_AcceptIdentity_NotCompressed()
|
public async Task Request_AcceptIdentity_NotCompressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "identity" }, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "identity" }, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
||||||
|
Assert.Equal(3, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type.");
|
||||||
|
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new string[] { "identity;q=0.5", "gzip;q=1" }, 24)]
|
[InlineData(new[] { "identity;q=0.5", "gzip;q=1" }, 24)]
|
||||||
[InlineData(new string[] { "identity;q=0", "gzip;q=0.8" }, 24)]
|
[InlineData(new[] { "identity;q=0", "gzip;q=0.8" }, 24)]
|
||||||
[InlineData(new string[] { "identity;q=0.5", "gzip" }, 24)]
|
[InlineData(new[] { "identity;q=0.5", "gzip" }, 24)]
|
||||||
public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
|
public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: expectedBodyLength);
|
CheckResponseCompressed(response, expectedBodyLength: expectedBodyLength, expectedEncoding: "gzip");
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new string[] { "gzip;q=0.5", "identity;q=0.8" }, 100)]
|
[InlineData(new[] { "gzip;q=0.5", "identity;q=0.8" }, 100)]
|
||||||
public async Task Request_AcceptWithhigherIdentityQuality_NotCompressed(string[] acceptEncodings, int expectedBodyLength)
|
public async Task Request_AcceptWithhigherIdentityQuality_NotCompressed(string[] acceptEncodings, int expectedBodyLength)
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: expectedBodyLength, sendVaryHeader: true);
|
CheckResponseNotCompressed(response, expectedBodyLength: expectedBodyLength, sendVaryHeader: true);
|
||||||
|
Assert.Equal(3, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type.");
|
||||||
|
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Response_UnknownMimeType_NotCompressed()
|
public async Task Response_UnknownMimeType_NotCompressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip" }, responseType: "text/custom");
|
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip" }, responseType: "text/custom");
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||||
|
Assert.Equal(2, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression is not enabled for the Content-Type 'text/custom'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Response_WithContentRange_NotCompressed()
|
public async Task Response_WithContentRange_NotCompressed()
|
||||||
{
|
{
|
||||||
var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
var (response, logMessages) = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
||||||
{
|
{
|
||||||
r.Headers[HeaderNames.ContentRange] = "1-2/*";
|
r.Headers[HeaderNames.ContentRange] = "1-2/*";
|
||||||
});
|
});
|
||||||
|
|
||||||
CheckResponseNotCompressed(response, expectedBodyLength: 50, sendVaryHeader: false);
|
CheckResponseNotCompressed(response, expectedBodyLength: 50, sendVaryHeader: false);
|
||||||
|
Assert.Equal(2, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression disabled due to the Content-Range header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Response_WithContentEncodingAlreadySet_Stacked()
|
public async Task Response_WithContentEncodingAlreadySet_NotReCompressed()
|
||||||
{
|
{
|
||||||
var otherContentEncoding = "something";
|
var otherContentEncoding = "something";
|
||||||
|
|
||||||
var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
var (response, logMessages) = await InvokeMiddleware(50, requestAcceptEncodings: new[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
||||||
{
|
{
|
||||||
r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding;
|
r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding;
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding));
|
Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding));
|
||||||
Assert.True(response.Content.Headers.ContentEncoding.Contains("gzip"));
|
Assert.False(response.Content.Headers.ContentEncoding.Contains("gzip"));
|
||||||
Assert.Equal(24, response.Content.Headers.ContentLength);
|
Assert.Equal(50, response.Content.Headers.ContentLength);
|
||||||
|
Assert.Equal(2, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Debug, "Response compression disabled due to the Content-Encoding header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -263,9 +436,15 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
[InlineData(true, 24)]
|
[InlineData(true, 24)]
|
||||||
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
|
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
|
||||||
{
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
|
||||||
|
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
services.AddResponseCompression(options =>
|
services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
options.EnableForHttps = enableHttps;
|
options.EnableForHttps = enableHttps;
|
||||||
|
|
@ -282,8 +461,11 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var server = new TestServer(builder);
|
var server = new TestServer(builder)
|
||||||
server.BaseAddress = new Uri("https://localhost/");
|
{
|
||||||
|
BaseAddress = new Uri("https://localhost/")
|
||||||
|
};
|
||||||
|
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
|
|
@ -292,10 +474,21 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
|
Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
|
||||||
|
|
||||||
|
var logMessages = sink.Writes.ToList();
|
||||||
|
if (enableHttps)
|
||||||
|
{
|
||||||
|
AssertCompressedWithLog(logMessages, "gzip");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task FlushHeaders_SendsHeaders_Compresses()
|
[MemberData(nameof(SupportedEncodingsWithBodyLength))]
|
||||||
|
public async Task FlushHeaders_SendsHeaders_Compresses(string encoding, int expectedBodyLength)
|
||||||
{
|
{
|
||||||
var responseReceived = new ManualResetEvent(false);
|
var responseReceived = new ManualResetEvent(false);
|
||||||
|
|
||||||
|
|
@ -321,18 +514,19 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
responseReceived.Set();
|
responseReceived.Set();
|
||||||
|
|
||||||
await response.Content.LoadIntoBufferAsync();
|
await response.Content.LoadIntoBufferAsync();
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
CheckResponseCompressed(response, expectedBodyLength, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task FlushAsyncHeaders_SendsHeaders_Compresses()
|
[MemberData(nameof(SupportedEncodingsWithBodyLength))]
|
||||||
|
public async Task FlushAsyncHeaders_SendsHeaders_Compresses(string encoding, int expectedBodyLength)
|
||||||
{
|
{
|
||||||
var responseReceived = new ManualResetEvent(false);
|
var responseReceived = new ManualResetEvent(false);
|
||||||
|
|
||||||
|
|
@ -358,18 +552,19 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
responseReceived.Set();
|
responseReceived.Set();
|
||||||
|
|
||||||
await response.Content.LoadIntoBufferAsync();
|
await response.Content.LoadIntoBufferAsync();
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
CheckResponseCompressed(response, expectedBodyLength, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task FlushBody_CompressesAndFlushes()
|
[MemberData(nameof(SupportedEncodings))]
|
||||||
|
public async Task FlushBody_CompressesAndFlushes(string encoding)
|
||||||
{
|
{
|
||||||
var responseReceived = new ManualResetEvent(false);
|
var responseReceived = new ManualResetEvent(false);
|
||||||
|
|
||||||
|
|
@ -397,13 +592,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
IEnumerable<string> contentMD5 = null;
|
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out _));
|
||||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
Assert.Single(response.Content.Headers.ContentEncoding, encoding);
|
||||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStreamAsync();
|
var body = await response.Content.ReadAsStreamAsync();
|
||||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||||
|
|
@ -415,8 +609,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
Assert.True(read > 0);
|
Assert.True(read > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task FlushAsyncBody_CompressesAndFlushes()
|
[MemberData(nameof(SupportedEncodings))]
|
||||||
|
public async Task FlushAsyncBody_CompressesAndFlushes(string encoding)
|
||||||
{
|
{
|
||||||
var responseReceived = new ManualResetEvent(false);
|
var responseReceived = new ManualResetEvent(false);
|
||||||
|
|
||||||
|
|
@ -443,13 +638,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
IEnumerable<string> contentMD5 = null;
|
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out _));
|
||||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
Assert.Single(response.Content.Headers.ContentEncoding, encoding);
|
||||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStreamAsync();
|
var body = await response.Content.ReadAsStreamAsync();
|
||||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||||
|
|
@ -461,8 +655,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
Assert.True(read > 0);
|
Assert.True(read > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task TrickleWriteAndFlush_FlushesEachWrite()
|
[MemberData(nameof(SupportedEncodings))]
|
||||||
|
public async Task TrickleWriteAndFlush_FlushesEachWrite(string encoding)
|
||||||
{
|
{
|
||||||
var responseReceived = new[]
|
var responseReceived = new[]
|
||||||
{
|
{
|
||||||
|
|
@ -501,17 +696,16 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
#if NET461 // Flush not supported, compression disabled
|
#if NET461 // Flush not supported, compression disabled
|
||||||
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
||||||
Assert.Empty(response.Content.Headers.ContentEncoding);
|
Assert.Empty(response.Content.Headers.ContentEncoding);
|
||||||
#elif NETCOREAPP2_0 || NETCOREAPP2_1 // Flush supported, compression enabled
|
#elif NETCOREAPP2_2 // Flush supported, compression enabled
|
||||||
IEnumerable<string> contentMD5 = null;
|
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out _));
|
||||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
Assert.Single(response.Content.Headers.ContentEncoding, encoding);
|
||||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
|
||||||
#else
|
#else
|
||||||
#error Target frameworks need to be updated.
|
#error Target frameworks need to be updated.
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -527,8 +721,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task TrickleWriteAndFlushAsync_FlushesEachWrite()
|
[MemberData(nameof(SupportedEncodings))]
|
||||||
|
public async Task TrickleWriteAndFlushAsync_FlushesEachWrite(string encoding)
|
||||||
{
|
{
|
||||||
var responseReceived = new[]
|
var responseReceived = new[]
|
||||||
{
|
{
|
||||||
|
|
@ -566,17 +761,16 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
var client = server.CreateClient();
|
var client = server.CreateClient();
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
request.Headers.AcceptEncoding.ParseAdd(encoding);
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
#if NET461 // Flush not supported, compression disabled
|
#if NET461 // Flush not supported, compression disabled
|
||||||
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
||||||
Assert.Empty(response.Content.Headers.ContentEncoding);
|
Assert.Empty(response.Content.Headers.ContentEncoding);
|
||||||
#elif NETCOREAPP2_0 || NETCOREAPP2_1 // Flush supported, compression enabled
|
#elif NETCOREAPP2_2 // Flush supported, compression enabled
|
||||||
IEnumerable<string> contentMD5 = null;
|
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out _));
|
||||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
Assert.Single(response.Content.Headers.ContentEncoding, encoding);
|
||||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
|
||||||
#else
|
#else
|
||||||
#error Target framework needs to be updated
|
#error Target framework needs to be updated
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -705,7 +899,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 34);
|
CheckResponseCompressed(response, expectedBodyLength: 34, expectedEncoding: "gzip");
|
||||||
|
|
||||||
Assert.False(fakeSendFile.Invoked);
|
Assert.False(fakeSendFile.Invoked);
|
||||||
}
|
}
|
||||||
|
|
@ -749,17 +943,28 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
CheckResponseCompressed(response, expectedBodyLength: 40);
|
CheckResponseCompressed(response, expectedBodyLength: 40, expectedEncoding: "gzip");
|
||||||
|
|
||||||
Assert.False(fakeSendFile.Invoked);
|
Assert.False(fakeSendFile.Invoked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<HttpResponseMessage> InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action<HttpResponse> addResponseAction = null)
|
private async Task<(HttpResponseMessage, List<WriteContext>)> InvokeMiddleware(
|
||||||
|
int uncompressedBodyLength,
|
||||||
|
string[] requestAcceptEncodings,
|
||||||
|
string responseType,
|
||||||
|
Action<HttpResponse> addResponseAction = null,
|
||||||
|
Action<ResponseCompressionOptions> configure = null)
|
||||||
{
|
{
|
||||||
|
var sink = new TestSink(
|
||||||
|
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
|
||||||
|
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
|
||||||
|
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
services.AddResponseCompression();
|
services.AddResponseCompression(configure ?? (_ => { }));
|
||||||
|
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||||
})
|
})
|
||||||
.Configure(app =>
|
.Configure(app =>
|
||||||
{
|
{
|
||||||
|
|
@ -769,10 +974,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||||
context.Response.ContentType = responseType;
|
context.Response.ContentType = responseType;
|
||||||
Assert.Null(context.Features.Get<IHttpSendFileFeature>());
|
Assert.Null(context.Features.Get<IHttpSendFileFeature>());
|
||||||
if (addResponseAction != null)
|
addResponseAction?.Invoke(context.Response);
|
||||||
{
|
|
||||||
addResponseAction(context.Response);
|
|
||||||
}
|
|
||||||
return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
|
return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -786,13 +988,11 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i]));
|
request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.SendAsync(request);
|
return (await client.SendAsync(request), sink.Writes.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength)
|
private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength, string expectedEncoding)
|
||||||
{
|
{
|
||||||
IEnumerable<string> contentMD5 = null;
|
|
||||||
|
|
||||||
var containsVaryAcceptEncoding = false;
|
var containsVaryAcceptEncoding = false;
|
||||||
foreach (var value in response.Headers.GetValues(HeaderNames.Vary))
|
foreach (var value in response.Headers.GetValues(HeaderNames.Vary))
|
||||||
{
|
{
|
||||||
|
|
@ -803,8 +1003,8 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.True(containsVaryAcceptEncoding);
|
Assert.True(containsVaryAcceptEncoding);
|
||||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out _));
|
||||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
Assert.Single(response.Content.Headers.ContentEncoding, expectedEncoding);
|
||||||
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -832,6 +1032,20 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AssertLog(WriteContext log, LogLevel level, string message)
|
||||||
|
{
|
||||||
|
Assert.Equal(level, log.LogLevel);
|
||||||
|
Assert.Equal(message, log.State.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AssertCompressedWithLog(List<WriteContext> logMessages, string provider)
|
||||||
|
{
|
||||||
|
Assert.Equal(3, logMessages.Count);
|
||||||
|
AssertLog(logMessages.First(), LogLevel.Trace, "This request accepts compression.");
|
||||||
|
AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "Response compression is available for this Content-Type.");
|
||||||
|
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, $"The response will be compressed with '{provider}'.");
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeSendFileFeature : IHttpSendFileFeature
|
private class FakeSendFileFeature : IHttpSendFileFeature
|
||||||
{
|
{
|
||||||
private readonly Stream _innerBody;
|
private readonly Stream _innerBody;
|
||||||
|
|
@ -858,5 +1072,18 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly struct EncodingTestData
|
||||||
|
{
|
||||||
|
public EncodingTestData(string encodingName, int expectedBodyLength)
|
||||||
|
{
|
||||||
|
EncodingName = encodingName;
|
||||||
|
ExpectedBodyLength = expectedBodyLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EncodingName { get; }
|
||||||
|
|
||||||
|
public int ExpectedBodyLength { get; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||||
{
|
{
|
||||||
|
|
@ -216,7 +217,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||||
{
|
{
|
||||||
// If the type is an integer, verify operand is actually an int
|
// If the type is an integer, verify operand is actually an int
|
||||||
int res;
|
int res;
|
||||||
if (!int.TryParse(results.Operand, out res))
|
if (!int.TryParse(results.Operand, NumberStyles.None, CultureInfo.InvariantCulture, out res))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Rewrite.Internal.PatternSegments;
|
using Microsoft.AspNetCore.Rewrite.Internal.PatternSegments;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
|
namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
|
||||||
|
|
@ -181,7 +182,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
|
||||||
|
|
||||||
var res = context.Capture();
|
var res = context.Capture();
|
||||||
int index;
|
int index;
|
||||||
if (!int.TryParse(res, out index))
|
if (!int.TryParse(res, NumberStyles.None, CultureInfo.InvariantCulture, out index))
|
||||||
{
|
{
|
||||||
throw new FormatException(Resources.FormatError_InputParserInvalidInteger(res, context.Index));
|
throw new FormatException(Resources.FormatError_InputParserInvalidInteger(res, context.Index));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
@ -219,7 +220,7 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite
|
||||||
break;
|
break;
|
||||||
case ActionType.CustomResponse:
|
case ActionType.CustomResponse:
|
||||||
int statusCode;
|
int statusCode;
|
||||||
if (!int.TryParse(urlAction.Attribute(RewriteTags.StatusCode)?.Value, out statusCode))
|
if (!int.TryParse(urlAction.Attribute(RewriteTags.StatusCode)?.Value, NumberStyles.None, CultureInfo.InvariantCulture, out statusCode))
|
||||||
{
|
{
|
||||||
throw new InvalidUrlRewriteFormatException(urlAction, "A valid status code is required");
|
throw new InvalidUrlRewriteFormatException(urlAction, "A valid status code is required");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue