Skip to content

Support conditional compression #8239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public partial struct FeatureReference<T>
public T Fetch(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { throw null; }
public T Update(Microsoft.AspNetCore.Http.Features.IFeatureCollection features, T feature) { throw null; }
}
public enum HttpsCompressionMode
{
Compress = 2,
Default = 0,
DoNotCompress = 1,
}
public partial interface IFeatureCollection : System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, object>>, System.Collections.IEnumerable
{
bool IsReadOnly { get; }
Expand Down Expand Up @@ -207,6 +213,10 @@ public partial interface IHttpResponseTrailersFeature
{
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; set; }
}
public partial interface IHttpsCompressionFeature
{
Microsoft.AspNetCore.Http.Features.HttpsCompressionMode Mode { get; set; }
}
public partial interface IHttpSendFileFeature
{
System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellation);
Expand Down
28 changes: 28 additions & 0 deletions src/Http/Http.Features/src/HttpsCompressionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.

namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Use to dynamically control response compression for HTTPS requests.
/// </summary>
public enum HttpsCompressionMode
{
/// <summary>
/// No value has been specified, use the configured defaults.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a "default" default? I'm not sure where I'd look to find the "configured defaults". Is the default hard-coded, or implied somewhere such that we could document it here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I.e. the global value at ResponseCompressionOptions.EnableForHttps

/// </summary>
Default = 0,

/// <summary>
/// Opts out of compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
/// may expose security problems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any canonical or at least permanent place we could direct folks for more information? Do we have examples of other API ref docs that warn about potential security problems that we can follow form on?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// </summary>
DoNotCompress,

/// <summary>
/// Opts into compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
/// may expose security problems.
/// </summary>
Compress,
}
}
16 changes: 16 additions & 0 deletions src/Http/Http.Features/src/IHttpsCompressionFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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.

namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Configures response compression behavior for HTTPS on a per-request basis.
/// </summary>
public interface IHttpsCompressionFeature
{
/// <summary>
/// The <see cref="HttpsCompressionMode"/> to use.
/// </summary>
HttpsCompressionMode Mode { get; set; }
}
}
4 changes: 3 additions & 1 deletion src/Middleware/ResponseCompression/src/BodyWrapperStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <summary>
/// Stream wrapper that create specific compression stream only if necessary.
/// </summary>
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature, IHttpsCompressionFeature
{
private readonly HttpContext _context;
private readonly Stream _bodyOriginalStream;
Expand Down Expand Up @@ -46,6 +46,8 @@ internal ValueTask FinishCompressionAsync()
return _compressionStream?.DisposeAsync() ?? new ValueTask();
}

HttpsCompressionMode IHttpsCompressionFeature.Mode { get; set; } = HttpsCompressionMode.Default;

public override bool CanRead => false;

public override bool CanSeek => false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ public async Task Invoke(HttpContext context)
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>();
var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();

var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
originalBufferFeature, originalSendFileFeature, originalStartFeature);
context.Response.Body = bodyWrapperStream;
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
context.Features.Set<IHttpsCompressionFeature>(bodyWrapperStream);
if (originalSendFileFeature != null)
{
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
Expand All @@ -79,6 +81,7 @@ public async Task Invoke(HttpContext context)
{
context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature);
context.Features.Set(originalCompressionFeature);
if (originalSendFileFeature != null)
{
context.Features.Set(originalSendFileFeature);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// 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.

using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.ResponseCompression
{
Expand All @@ -22,8 +23,11 @@ public class ResponseCompressionOptions

/// <summary>
/// Indicates if responses over HTTPS connections should be compressed. The default is 'false'.
/// Enabling compression on HTTPS connections may expose security problems.
/// Enabling compression on HTTPS requests for remotely manipulable content may expose security problems.
/// </summary>
/// <remarks>
/// This can be overridden per request using <see cref="IHttpsCompressionFeature"/>.
/// </remarks>
public bool EnableForHttps { get; set; } = false;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCompression.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -170,6 +171,17 @@ public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
/// <inheritdoc />
public virtual bool ShouldCompressResponse(HttpContext context)
{
var httpsMode = context.Features.Get<IHttpsCompressionFeature>()?.Mode ?? HttpsCompressionMode.Default;

// Check if the app has opted into or out of compression over HTTPS
if (context.Request.IsHttps
&& (httpsMode == HttpsCompressionMode.DoNotCompress
|| !(_enableForHttps || httpsMode == HttpsCompressionMode.Compress)))
{
_logger.NoCompressionForHttps();
return false;
}

if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
{
_logger.NoCompressionDueToHeader(HeaderNames.ContentRange);
Expand Down Expand Up @@ -215,12 +227,6 @@ public virtual bool ShouldCompressResponse(HttpContext context)
/// <inheritdoc />
public bool CheckRequestAcceptsCompression(HttpContext context)
{
if (context.Request.IsHttps && !_enableForHttps)
{
_logger.NoCompressionForHttps();
return false;
}

if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
{
_logger.NoAcceptEncoding();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,123 @@ public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expect
}
else
{
AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
}
}

[Theory]
[InlineData(HttpsCompressionMode.Default, 100)]
[InlineData(HttpsCompressionMode.DoNotCompress, 100)]
[InlineData(HttpsCompressionMode.Compress, 30)]
public async Task Request_Https_CompressedIfOptIn(HttpsCompressionMode mode, int expectedLength)
{
var sink = new TestSink(
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);

var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<ILoggerFactory>(loggerFactory);
services.AddResponseCompression(options =>
{
options.EnableForHttps = false;
options.MimeTypes = new[] { TextPlain };
});
})
.Configure(app =>
{
app.UseResponseCompression();
app.Run(context =>
{
var feature = context.Features.Get<IHttpsCompressionFeature>();
feature.Mode = mode;
context.Response.ContentType = TextPlain;
return context.Response.WriteAsync(new string('a', 100));
});
});

var server = new TestServer(builder)
{
BaseAddress = new Uri("https://localhost/")
};

var client = server.CreateClient();

var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");

var response = await client.SendAsync(request);

Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);

var logMessages = sink.Writes.ToList();
if (mode == HttpsCompressionMode.Compress)
{
AssertCompressedWithLog(logMessages, "gzip");
}
else
{
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
}
}

[Theory]
[InlineData(HttpsCompressionMode.Default, 30)]
[InlineData(HttpsCompressionMode.Compress, 30)]
[InlineData(HttpsCompressionMode.DoNotCompress, 100)]
public async Task Request_Https_NotCompressedIfOptOut(HttpsCompressionMode mode, int expectedLength)
{
var sink = new TestSink(
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
var loggerFactory = new TestLoggerFactory(sink, enabled: true);

var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<ILoggerFactory>(loggerFactory);
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.MimeTypes = new[] { TextPlain };
});
})
.Configure(app =>
{
app.UseResponseCompression();
app.Run(context =>
{
var feature = context.Features.Get<IHttpsCompressionFeature>();
feature.Mode = mode;
context.Response.ContentType = TextPlain;
return context.Response.WriteAsync(new string('a', 100));
});
});

var server = new TestServer(builder)
{
BaseAddress = new Uri("https://localhost/")
};

var client = server.CreateClient();

var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");

var response = await client.SendAsync(request);

Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);

var logMessages = sink.Writes.ToList();
if (mode == HttpsCompressionMode.DoNotCompress)
{
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
}
else
{
AssertCompressedWithLog(logMessages, "gzip");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public StaticFileOptions() : base (default(Microsoft.AspNetCore.StaticFiles.Infr
public StaticFileOptions(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) : base (default(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions)) { }
public Microsoft.AspNetCore.StaticFiles.IContentTypeProvider ContentTypeProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string DefaultContentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Features.HttpsCompressionMode HttpsCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> OnPrepareResponse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ public class Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDirectoryBrowser();
services.AddResponseCompression();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment host)
{
Console.WriteLine("webroot: " + host.WebRootPath);

app.UseResponseCompression();

app.UseFileServer(new FileServerOptions
{
EnableDirectoryBrowsing = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
Expand Down
12 changes: 12 additions & 0 deletions src/Middleware/StaticFiles/src/StaticFileContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public Task SendStatusAsync(int statusCode)

public async Task SendAsync()
{
SetCompressionMode();
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
Expand Down Expand Up @@ -366,6 +367,7 @@ internal async Task SendRangeAsync()

_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length;
SetCompressionMode();
ApplyResponseHeaders(Constants.Status206PartialContent);

string physicalPath = _fileInfo.PhysicalPath;
Expand Down Expand Up @@ -404,5 +406,15 @@ private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range,
length = end - start + 1;
return new ContentRangeHeaderValue(start, end, _length);
}

// Only called when we expect to serve the body.
private void SetCompressionMode()
{
var responseCompressionFeature = _context.Features.Get<IHttpsCompressionFeature>();
if (responseCompressionFeature != null)
{
responseCompressionFeature.Mode = _options.HttpsCompression;
}
}
}
}
10 changes: 10 additions & 0 deletions src/Middleware/StaticFiles/src/StaticFileOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.StaticFiles.Infrastructure;

Expand Down Expand Up @@ -46,6 +47,15 @@ public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
/// </summary>
public bool ServeUnknownFileTypes { get; set; }

/// <summary>
/// Indicates if files should be compressed for HTTPS requests when the Response Compression middleware is available.
/// The default value is <see cref="HttpsCompressionMode.Compress"/>.
/// </summary>
/// <remarks>
/// Enabling compression on HTTPS requests for remotely manipulable content may expose security problems.
/// </remarks>
public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;

/// <summary>
/// Called after the status code and headers have been set, but before the body has been written.
/// This can be used to add or change the response headers.
Expand Down
Loading