Support conditional compression #6925 (#8239)

This commit is contained in:
Chris Ross 2019-03-07 11:09:48 -08:00 committed by GitHub
parent 7fb3d57f54
commit 6db129a588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 280 additions and 13 deletions

View File

@ -131,6 +131,12 @@ namespace Microsoft.AspNetCore.Http.Features
public T Fetch(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { throw null; } 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 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 public partial interface IFeatureCollection : System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, object>>, System.Collections.IEnumerable
{ {
bool IsReadOnly { get; } bool IsReadOnly { get; }
@ -207,6 +213,10 @@ namespace Microsoft.AspNetCore.Http.Features
{ {
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; set; } Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; set; }
} }
public partial interface IHttpsCompressionFeature
{
Microsoft.AspNetCore.Http.Features.HttpsCompressionMode Mode { get; set; }
}
public partial interface IHttpSendFileFeature public partial interface IHttpSendFileFeature
{ {
System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellation); System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellation);

View File

@ -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.
/// </summary>
Default = 0,
/// <summary>
/// Opts out of compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
/// may expose security problems.
/// </summary>
DoNotCompress,
/// <summary>
/// Opts into compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content
/// may expose security problems.
/// </summary>
Compress,
}
}

View File

@ -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; }
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <summary> /// <summary>
/// Stream wrapper that create specific compression stream only if necessary. /// Stream wrapper that create specific compression stream only if necessary.
/// </summary> /// </summary>
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature, IHttpsCompressionFeature
{ {
private readonly HttpContext _context; private readonly HttpContext _context;
private readonly Stream _bodyOriginalStream; private readonly Stream _bodyOriginalStream;
@ -46,6 +46,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
return _compressionStream?.DisposeAsync() ?? new ValueTask(); return _compressionStream?.DisposeAsync() ?? new ValueTask();
} }
HttpsCompressionMode IHttpsCompressionFeature.Mode { get; set; } = HttpsCompressionMode.Default;
public override bool CanRead => false; public override bool CanRead => false;
public override bool CanSeek => false; public override bool CanSeek => false;

View File

@ -55,11 +55,13 @@ namespace Microsoft.AspNetCore.ResponseCompression
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>(); var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>(); var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>(); var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>();
var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider, var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
originalBufferFeature, originalSendFileFeature, originalStartFeature); originalBufferFeature, originalSendFileFeature, originalStartFeature);
context.Response.Body = bodyWrapperStream; context.Response.Body = bodyWrapperStream;
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream); context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
context.Features.Set<IHttpsCompressionFeature>(bodyWrapperStream);
if (originalSendFileFeature != null) if (originalSendFileFeature != null)
{ {
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream); context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
@ -79,6 +81,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
{ {
context.Response.Body = bodyStream; context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature); context.Features.Set(originalBufferFeature);
context.Features.Set(originalCompressionFeature);
if (originalSendFileFeature != null) if (originalSendFileFeature != null)
{ {
context.Features.Set(originalSendFileFeature); context.Features.Set(originalSendFileFeature);

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.ResponseCompression namespace Microsoft.AspNetCore.ResponseCompression
{ {
@ -22,8 +23,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <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'.
/// Enabling compression on HTTPS connections may expose security problems. /// Enabling compression on HTTPS requests for remotely manipulable content may expose security problems.
/// </summary> /// </summary>
/// <remarks>
/// This can be overridden per request using <see cref="IHttpsCompressionFeature"/>.
/// </remarks>
public bool EnableForHttps { get; set; } = false; public bool EnableForHttps { get; set; } = false;
/// <summary> /// <summary>

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCompression.Internal; using Microsoft.AspNetCore.ResponseCompression.Internal;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -170,6 +171,17 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <inheritdoc /> /// <inheritdoc />
public virtual bool ShouldCompressResponse(HttpContext context) 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)) if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
{ {
_logger.NoCompressionDueToHeader(HeaderNames.ContentRange); _logger.NoCompressionDueToHeader(HeaderNames.ContentRange);
@ -215,12 +227,6 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <inheritdoc /> /// <inheritdoc />
public bool CheckRequestAcceptsCompression(HttpContext context) public bool CheckRequestAcceptsCompression(HttpContext context)
{ {
if (context.Request.IsHttps && !_enableForHttps)
{
_logger.NoCompressionForHttps();
return false;
}
if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding])) if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
{ {
_logger.NoAcceptEncoding(); _logger.NoAcceptEncoding();

View File

@ -453,7 +453,123 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
} }
else 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");
} }
} }

View File

@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore.Builder
public StaticFileOptions(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions sharedOptions) : base (default(Microsoft.AspNetCore.StaticFiles.Infrastructure.SharedOptions)) { } 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 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 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 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 { } } public bool ServeUnknownFileTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
} }

View File

@ -12,12 +12,15 @@ namespace StaticFilesSample
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddDirectoryBrowser(); services.AddDirectoryBrowser();
services.AddResponseCompression();
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment host) public void Configure(IApplicationBuilder app, IWebHostEnvironment host)
{ {
Console.WriteLine("webroot: " + host.WebRootPath); Console.WriteLine("webroot: " + host.WebRootPath);
app.UseResponseCompression();
app.UseFileServer(new FileServerOptions app.UseFileServer(new FileServerOptions
{ {
EnableDirectoryBrowsing = true EnableDirectoryBrowsing = true

View File

@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" /> <Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" /> <Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" /> <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />

View File

@ -322,6 +322,7 @@ namespace Microsoft.AspNetCore.StaticFiles
public async Task SendAsync() public async Task SendAsync()
{ {
SetCompressionMode();
ApplyResponseHeaders(Constants.Status200Ok); ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath; string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>(); var sendFile = _context.Features.Get<IHttpSendFileFeature>();
@ -366,6 +367,7 @@ namespace Microsoft.AspNetCore.StaticFiles
_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length); _responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length; _response.ContentLength = length;
SetCompressionMode();
ApplyResponseHeaders(Constants.Status206PartialContent); ApplyResponseHeaders(Constants.Status206PartialContent);
string physicalPath = _fileInfo.PhysicalPath; string physicalPath = _fileInfo.PhysicalPath;
@ -404,5 +406,15 @@ namespace Microsoft.AspNetCore.StaticFiles
length = end - start + 1; length = end - start + 1;
return new ContentRangeHeaderValue(start, end, _length); 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;
}
}
} }
} }

View File

@ -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 Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.StaticFiles.Infrastructure; using Microsoft.AspNetCore.StaticFiles.Infrastructure;
@ -46,6 +47,15 @@ namespace Microsoft.AspNetCore.Builder
/// </summary> /// </summary>
public bool ServeUnknownFileTypes { get; set; } 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> /// <summary>
/// Called after the status code and headers have been set, but before the body has been written. /// 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. /// This can be used to add or change the response headers.

View File

@ -4,8 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -54,6 +56,54 @@ namespace Microsoft.AspNetCore.StaticFiles
Assert.True(result); Assert.True(result);
} }
[Fact]
public async Task EnablesHttpsCompression_IfMatched()
{
var options = new StaticFileOptions();
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/foo.txt", new TestFileInfo
{
LastModified = new DateTimeOffset(2014, 1, 2, 3, 4, 5, TimeSpan.Zero)
});
var pathString = new PathString("/test");
var httpContext = new DefaultHttpContext();
var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/foo.txt");
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
context.ValidatePath();
var result = context.LookupFileInfo();
Assert.True(result);
await context.SendAsync();
Assert.Equal(HttpsCompressionMode.Compress, httpsCompressionFeature.Mode);
}
[Fact]
public void SkipsHttpsCompression_IfNotMatched()
{
var options = new StaticFileOptions();
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/foo.txt", new TestFileInfo
{
LastModified = new DateTimeOffset(2014, 1, 2, 3, 4, 5, TimeSpan.Zero)
});
var pathString = new PathString("/test");
var httpContext = new DefaultHttpContext();
var httpsCompressionFeature = new TestHttpsCompressionFeature();
httpContext.Features.Set<IHttpsCompressionFeature>(httpsCompressionFeature);
httpContext.Request.Path = new PathString("/test/bar.txt");
var context = new StaticFileContext(httpContext, options, pathString, NullLogger.Instance, fileProvider, new FileExtensionContentTypeProvider());
context.ValidatePath();
var result = context.LookupFileInfo();
Assert.False(result);
Assert.Equal(HttpsCompressionMode.Default, httpsCompressionFeature.Mode);
}
private sealed class TestFileProvider : IFileProvider private sealed class TestFileProvider : IFileProvider
{ {
private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>(StringComparer.Ordinal); private readonly Dictionary<string, IFileInfo> _files = new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
@ -162,8 +212,13 @@ namespace Microsoft.AspNetCore.StaticFiles
public Stream CreateReadStream() public Stream CreateReadStream()
{ {
throw new NotImplementedException(); return new MemoryStream();
} }
} }
private class TestHttpsCompressionFeature : IHttpsCompressionFeature
{
public HttpsCompressionMode Mode { get; set; }
}
} }
} }