Skip to content

Commit ab968f7

Browse files
committed
Support conditional compression #6925
1 parent 5170c31 commit ab968f7

14 files changed

+282
-13
lines changed

src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ public partial struct FeatureReference<T>
131131
public T Fetch(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { throw null; }
132132
public T Update(Microsoft.AspNetCore.Http.Features.IFeatureCollection features, T feature) { throw null; }
133133
}
134+
public enum HttpsCompressionMode
135+
{
136+
Compress = 2,
137+
Default = 0,
138+
DoNotCompress = 1,
139+
}
134140
public partial interface IFeatureCollection : System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, object>>, System.Collections.IEnumerable
135141
{
136142
bool IsReadOnly { get; }
@@ -207,6 +213,10 @@ public partial interface IHttpResponseTrailersFeature
207213
{
208214
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; set; }
209215
}
216+
public partial interface IHttpsCompressionFeature
217+
{
218+
Microsoft.AspNetCore.Http.Features.HttpsCompressionMode Mode { get; set; }
219+
}
210220
public partial interface IHttpSendFileFeature
211221
{
212222
System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellation);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Http.Features
5+
{
6+
/// <summary>
7+
/// Use to dynamically control response compression for HTTPS requests.
8+
/// </summary>
9+
public enum HttpsCompressionMode
10+
{
11+
/// <summary>
12+
/// No value has been specified, use the configured defaults.
13+
/// </summary>
14+
Default,
15+
16+
/// <summary>
17+
/// Opts out of compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
18+
/// may expose security problems.
19+
/// </summary>
20+
DoNotCompress,
21+
22+
/// <summary>
23+
/// Opts into compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
24+
/// may expose security problems.
25+
/// </summary>
26+
Compress,
27+
}
28+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Http.Features
5+
{
6+
/// <summary>
7+
/// Configures response compression behavior for HTTPS on a per-request basis.
8+
/// </summary>
9+
public interface IHttpsCompressionFeature
10+
{
11+
/// <summary>
12+
/// The <see cref="HttpsCompressionMode"/> to use.
13+
/// </summary>
14+
HttpsCompressionMode Mode { get; set; }
15+
}
16+
}

src/Middleware/ResponseCompression/src/BodyWrapperStream.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
1515
/// <summary>
1616
/// Stream wrapper that create specific compression stream only if necessary.
1717
/// </summary>
18-
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature
18+
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature, IHttpsCompressionFeature
1919
{
2020
private readonly HttpContext _context;
2121
private readonly Stream _bodyOriginalStream;
@@ -46,6 +46,8 @@ internal ValueTask FinishCompressionAsync()
4646
return _compressionStream?.DisposeAsync() ?? new ValueTask();
4747
}
4848

49+
HttpsCompressionMode IHttpsCompressionFeature.Mode { get; set; }
50+
4951
public override bool CanRead => false;
5052

5153
public override bool CanSeek => false;

src/Middleware/ResponseCompression/src/ResponseCompressionMiddleware.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ public async Task Invoke(HttpContext context)
5555
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
5656
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
5757
var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>();
58+
var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();
5859

5960
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
6061
originalBufferFeature, originalSendFileFeature, originalStartFeature);
6162
context.Response.Body = bodyWrapperStream;
6263
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
64+
context.Features.Set<IHttpsCompressionFeature>(bodyWrapperStream);
6365
if (originalSendFileFeature != null)
6466
{
6567
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
@@ -79,6 +81,7 @@ public async Task Invoke(HttpContext context)
7981
{
8082
context.Response.Body = bodyStream;
8183
context.Features.Set(originalBufferFeature);
84+
context.Features.Set(originalCompressionFeature);
8285
if (originalSendFileFeature != null)
8386
{
8487
context.Features.Set(originalSendFileFeature);

src/Middleware/ResponseCompression/src/ResponseCompressionOptions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.Collections.Generic;
5+
using Microsoft.AspNetCore.Http.Features;
56

67
namespace Microsoft.AspNetCore.ResponseCompression
78
{
@@ -22,8 +23,11 @@ public class ResponseCompressionOptions
2223

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

2933
/// <summary>

src/Middleware/ResponseCompression/src/ResponseCompressionProvider.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Linq;
88
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.ResponseCompression.Internal;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Logging;
@@ -170,6 +171,16 @@ public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
170171
/// <inheritdoc />
171172
public virtual bool ShouldCompressResponse(HttpContext context)
172173
{
174+
var httpsMode = context.Features.Get<IHttpsCompressionFeature>()?.Mode ?? HttpsCompressionMode.Default;
175+
176+
if (context.Request.IsHttps
177+
&& (httpsMode == HttpsCompressionMode.DoNotCompress
178+
|| !(_enableForHttps || httpsMode == HttpsCompressionMode.Compress)))
179+
{
180+
_logger.NoCompressionForHttps();
181+
return false;
182+
}
183+
173184
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
174185
{
175186
_logger.NoCompressionDueToHeader(HeaderNames.ContentRange);
@@ -215,12 +226,6 @@ public virtual bool ShouldCompressResponse(HttpContext context)
215226
/// <inheritdoc />
216227
public bool CheckRequestAcceptsCompression(HttpContext context)
217228
{
218-
if (context.Request.IsHttps && !_enableForHttps)
219-
{
220-
_logger.NoCompressionForHttps();
221-
return false;
222-
}
223-
224229
if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
225230
{
226231
_logger.NoAcceptEncoding();

src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,127 @@ public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expect
453453
}
454454
else
455455
{
456-
AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
456+
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
457+
}
458+
}
459+
460+
[Theory]
461+
[InlineData(false, 100)]
462+
[InlineData(true, 30)]
463+
public async Task Request_Https_CompressedIfOverriden(bool overrideHttps, int expectedLength)
464+
{
465+
var sink = new TestSink(
466+
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
467+
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
468+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
469+
470+
var builder = new WebHostBuilder()
471+
.ConfigureServices(services =>
472+
{
473+
services.AddSingleton<ILoggerFactory>(loggerFactory);
474+
services.AddResponseCompression(options =>
475+
{
476+
options.EnableForHttps = false;
477+
options.MimeTypes = new[] { TextPlain };
478+
});
479+
})
480+
.Configure(app =>
481+
{
482+
app.UseResponseCompression();
483+
app.Run(context =>
484+
{
485+
if (overrideHttps)
486+
{
487+
var feature = context.Features.Get<IHttpsCompressionFeature>();
488+
feature.Mode = HttpsCompressionMode.Compress;
489+
}
490+
context.Response.ContentType = TextPlain;
491+
return context.Response.WriteAsync(new string('a', 100));
492+
});
493+
});
494+
495+
var server = new TestServer(builder)
496+
{
497+
BaseAddress = new Uri("https://localhost/")
498+
};
499+
500+
var client = server.CreateClient();
501+
502+
var request = new HttpRequestMessage(HttpMethod.Get, "");
503+
request.Headers.AcceptEncoding.ParseAdd("gzip");
504+
505+
var response = await client.SendAsync(request);
506+
507+
Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
508+
509+
var logMessages = sink.Writes.ToList();
510+
if (overrideHttps)
511+
{
512+
AssertCompressedWithLog(logMessages, "gzip");
513+
}
514+
else
515+
{
516+
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
517+
}
518+
}
519+
520+
[Theory]
521+
[InlineData(true, 100)]
522+
[InlineData(false, 30)]
523+
public async Task Request_Https_NotCompressedIfOverriden(bool overrideHttps, int expectedLength)
524+
{
525+
var sink = new TestSink(
526+
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
527+
TestSink.EnableWithTypeName<ResponseCompressionProvider>);
528+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
529+
530+
var builder = new WebHostBuilder()
531+
.ConfigureServices(services =>
532+
{
533+
services.AddSingleton<ILoggerFactory>(loggerFactory);
534+
services.AddResponseCompression(options =>
535+
{
536+
options.EnableForHttps = true;
537+
options.MimeTypes = new[] { TextPlain };
538+
});
539+
})
540+
.Configure(app =>
541+
{
542+
app.UseResponseCompression();
543+
app.Run(context =>
544+
{
545+
if (overrideHttps)
546+
{
547+
var feature = context.Features.Get<IHttpsCompressionFeature>();
548+
feature.Mode = HttpsCompressionMode.DoNotCompress;
549+
}
550+
context.Response.ContentType = TextPlain;
551+
return context.Response.WriteAsync(new string('a', 100));
552+
});
553+
});
554+
555+
var server = new TestServer(builder)
556+
{
557+
BaseAddress = new Uri("https://localhost/")
558+
};
559+
560+
var client = server.CreateClient();
561+
562+
var request = new HttpRequestMessage(HttpMethod.Get, "");
563+
request.Headers.AcceptEncoding.ParseAdd("gzip");
564+
565+
var response = await client.SendAsync(request);
566+
567+
Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
568+
569+
var logMessages = sink.Writes.ToList();
570+
if (overrideHttps)
571+
{
572+
AssertLog(logMessages.Skip(1).Single(), LogLevel.Debug, "No response compression available for HTTPS requests. See ResponseCompressionOptions.EnableForHttps.");
573+
}
574+
else
575+
{
576+
AssertCompressedWithLog(logMessages, "gzip");
457577
}
458578
}
459579

src/Middleware/StaticFiles/ref/Microsoft.AspNetCore.StaticFiles.netcoreapp3.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public StaticFileOptions() : base (default(Microsoft.AspNetCore.StaticFiles.Infr
5555
public StaticFileOptions(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) : base (default(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions)) { }
5656
public Microsoft.AspNetCore.StaticFiles.IContentTypeProvider ContentTypeProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5757
public string DefaultContentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
58+
public Microsoft.AspNetCore.Http.Features.HttpsCompressionMode HttpsCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5859
public System.Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> OnPrepareResponse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5960
public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
6061
}

src/Middleware/StaticFiles/samples/StaticFileSample/Startup.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ public class Startup
1212
public void ConfigureServices(IServiceCollection services)
1313
{
1414
services.AddDirectoryBrowser();
15+
services.AddResponseCompression();
1516
}
1617

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

22+
app.UseResponseCompression();
23+
2124
app.UseFileServer(new FileServerOptions
2225
{
2326
EnableDirectoryBrowsing = true

src/Middleware/StaticFiles/samples/StaticFileSample/StaticFileSample.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.0</TargetFramework>
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
89
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
910
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
1011
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />

src/Middleware/StaticFiles/src/StaticFileContext.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ public Task SendStatusAsync(int statusCode)
322322

323323
public async Task SendAsync()
324324
{
325+
SetCompressionMode();
325326
ApplyResponseHeaders(Constants.Status200Ok);
326327
string physicalPath = _fileInfo.PhysicalPath;
327328
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
@@ -366,6 +367,7 @@ internal async Task SendRangeAsync()
366367

367368
_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
368369
_response.ContentLength = length;
370+
SetCompressionMode();
369371
ApplyResponseHeaders(Constants.Status206PartialContent);
370372

371373
string physicalPath = _fileInfo.PhysicalPath;
@@ -404,5 +406,15 @@ private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range,
404406
length = end - start + 1;
405407
return new ContentRangeHeaderValue(start, end, _length);
406408
}
409+
410+
// Only called when we expect to serve the body.
411+
private void SetCompressionMode()
412+
{
413+
var responseCompressionFeature = _context.Features.Get<IHttpsCompressionFeature>();
414+
if (responseCompressionFeature != null)
415+
{
416+
responseCompressionFeature.Mode = _options.HttpsCompression;
417+
}
418+
}
407419
}
408420
}

src/Middleware/StaticFiles/src/StaticFileOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using Microsoft.AspNetCore.Http.Features;
56
using Microsoft.AspNetCore.StaticFiles;
67
using Microsoft.AspNetCore.StaticFiles.Infrastructure;
78

@@ -46,6 +47,14 @@ public StaticFileOptions(SharedOptions sharedOptions) : base(sharedOptions)
4647
/// </summary>
4748
public bool ServeUnknownFileTypes { get; set; }
4849

50+
/// <summary>
51+
/// Indicates if files should be compressed for HTTPS requests. The default value is <see cref="HttpsCompressionMode.Compress"/>.
52+
/// </summary>
53+
/// <remarks>
54+
/// Enabling compression on HTTPS requests for remotely manipulable content may expose security problems.
55+
/// </remarks>
56+
public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;
57+
4958
/// <summary>
5059
/// Called after the status code and headers have been set, but before the body has been written.
5160
/// This can be used to add or change the response headers.

0 commit comments

Comments
 (0)