Skip to content
This repository was archived by the owner on Dec 20, 2018. It is now read-only.

Response Compression middleware #52

Closed
wants to merge 11 commits into from
Closed
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
21 changes: 21 additions & 0 deletions BasicMiddleware.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RewriteSample", "samples\Re
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite.Tests", "test\Microsoft.AspNetCore.Rewrite.Tests\Microsoft.AspNetCore.Rewrite.Tests.xproj", "{31794F9E-A1AA-4535-B03C-A3233737CD1A}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCompression", "src\Microsoft.AspNetCore.ResponseCompression\Microsoft.AspNetCore.ResponseCompression.xproj", "{45308A9D-F4C6-46A8-A24F-E73D995CC223}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCompression.Tests", "test\Microsoft.AspNetCore.ResponseCompression.Tests\Microsoft.AspNetCore.ResponseCompression.Tests.xproj", "{3360A5D1-70C0-49EE-9051-04A6A6B836DC}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCompressionSample", "samples\ResponseCompressionSample\ResponseCompressionSample.xproj", "{B2A3CE38-51B2-4486-982C-98C380AF140E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +80,18 @@ Global
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.Build.0 = Release|Any CPU
{45308A9D-F4C6-46A8-A24F-E73D995CC223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45308A9D-F4C6-46A8-A24F-E73D995CC223}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45308A9D-F4C6-46A8-A24F-E73D995CC223}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45308A9D-F4C6-46A8-A24F-E73D995CC223}.Release|Any CPU.Build.0 = Release|Any CPU
{3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Release|Any CPU.Build.0 = Release|Any CPU
{B2A3CE38-51B2-4486-982C-98C380AF140E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2A3CE38-51B2-4486-982C-98C380AF140E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -88,5 +106,8 @@ Global
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
{9E049645-13BC-4598-89E1-5B43D36E5D14} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{31794F9E-A1AA-4535-B03C-A3233737CD1A} = {8437B0F3-3894-4828-A945-A9187F37631D}
{45308A9D-F4C6-46A8-A24F-E73D995CC223} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
{3360A5D1-70C0-49EE-9051-04A6A6B836DC} = {8437B0F3-3894-4828-A945-A9187F37631D}
{B2A3CE38-51B2-4486-982C-98C380AF140E} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
EndGlobalSection
EndGlobal
12 changes: 12 additions & 0 deletions samples/ResponseCompressionSample/LoremIpsum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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 ResponseCompressionSample
{
internal static class LoremIpsum
{
internal const string Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit." +
"Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar.Nulla sollicitudin.Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula.Pellentesque rhoncus nunc et augue.Integer id felis.Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas.Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Morbi vel erat non mauris convallis vehicula.Nulla et sapien.Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam.Mauris ullamcorper felis vitae erat.Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna." +
"Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien.Vivamus leo. Aliquam euismod libero eu enim.Nulla nec felis sed leo placerat imperdiet.Aenean suscipit nulla in justo.Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.";
}
}
25 changes: 25 additions & 0 deletions samples/ResponseCompressionSample/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:49658/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"web": {
"commandName": "web",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
18 changes: 18 additions & 0 deletions samples/ResponseCompressionSample/ResponseCompressionSample.xproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>b2a3ce38-51b2-4486-982c-98c380af140e</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>46824</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
37 changes: 37 additions & 0 deletions samples/ResponseCompressionSample/Startup.cs
Original file line number Diff line number Diff line change
@@ -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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCompression;

namespace ResponseCompressionSample
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseResponseCompression(new ResponseCompressionOptions()
{
ShouldCompressResponse = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { "text/plain" })
});

app.Run(async context =>
{
context.Response.Headers["Content-Type"] = "text/plain";
await context.Response.WriteAsync(LoremIpsum.Text);
});
}

public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseStartup<Startup>()
.Build();

host.Run();
}
}
}
31 changes: 31 additions & 0 deletions samples/ResponseCompressionSample/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": "1.1.0-*",
"dependencies": {
"Microsoft.AspNetCore.ResponseCompression": "0.1.0-*",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*"
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"frameworks": {
"net451": {},
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0-*",
"type": "platform"
}
}
}
},
"publish": {
"exclude": [
"node_modules",
"bower_components",
"**.xproj",
"**.user",
"**.vspscc"
]
}
}
192 changes: 192 additions & 0 deletions src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// 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;
Copy link

Choose a reason for hiding this comment

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

Please put System.* ahead of other namespaces.

using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.ResponseCompression
{
/// <summary>
/// Stream wrapper that create specific compression stream only if necessary.
/// </summary>
internal class BodyWrapperStream : Stream
{
private readonly HttpResponse _response;

private readonly Stream _bodyOriginalStream;

private readonly Func<HttpContext, bool> _shouldCompressResponse;

private readonly IResponseCompressionProvider _compressionProvider;

private bool _compressionChecked = false;

private Stream _compressionStream = null;

internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, Func<HttpContext, bool> shouldCompressResponse, IResponseCompressionProvider compressionProvider)
{
_response = response;
_bodyOriginalStream = bodyOriginalStream;
_shouldCompressResponse = shouldCompressResponse;
_compressionProvider = compressionProvider;
}

protected override void Dispose(bool disposing)
{
if (_compressionStream != null)
{
_compressionStream.Dispose();
_compressionStream = null;
}
}

public override bool CanRead => _bodyOriginalStream.CanRead;

public override bool CanSeek => _bodyOriginalStream.CanSeek;

public override bool CanWrite => _bodyOriginalStream.CanWrite;

public override long Length
{
get
{
throw new NotSupportedException();
}
}

public override long Position
{
get
{
throw new NotSupportedException();
}

set
{
throw new NotSupportedException();
}
}

public override void Flush()
{
OnWrite();

if (_compressionStream != null)
Copy link
Member

Choose a reason for hiding this comment

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

OnWrite();

{
_compressionStream.Flush();
}
else
{
_bodyOriginalStream.Flush();
}
}

public override Task FlushAsync(CancellationToken cancellationToken)
{
OnWrite();

if (_compressionStream != null)
Copy link
Member

Choose a reason for hiding this comment

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

This needs to call OnWrite() if the user is just trying to flush the headers

{
return _compressionStream.FlushAsync(cancellationToken);
}
return _bodyOriginalStream.FlushAsync(cancellationToken);
}

public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}

public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}

public override void SetLength(long value)
{
throw new NotSupportedException();
}

public override void Write(byte[] buffer, int offset, int count)
{
OnWrite();

if (_compressionStream != null)
{
_compressionStream.Write(buffer, offset, count);
}
else
{
_bodyOriginalStream.Write(buffer, offset, count);
}
}

#if NET451
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
OnWrite();

if (_compressionStream != null)
{
return _compressionStream.BeginWrite(buffer, offset, count, callback, state);
}
return _bodyOriginalStream.BeginWrite(buffer, offset, count, callback, state);
}

public override void EndWrite(IAsyncResult asyncResult)
{
OnWrite();

if (_compressionStream != null)
{
_compressionStream.EndWrite(asyncResult);
}
else
{
_bodyOriginalStream.EndWrite(asyncResult);
}
}
#endif

public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
OnWrite();

if (_compressionStream != null)
{
return _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
}
return _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
}

private void OnWrite()
{
if (!_compressionChecked)
{
if (IsCompressable())
{
_response.Headers[HeaderNames.ContentEncoding] = _compressionProvider.EncodingName;
_response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
_response.Headers.Remove(HeaderNames.ContentLength);

_compressionStream = _compressionProvider.CreateStream(_bodyOriginalStream);
}

_compressionChecked = true;
}
}

private bool IsCompressable()
{
return _response.Headers[HeaderNames.ContentRange] == StringValues.Empty && // The response is not partial
_response.Headers[HeaderNames.ContentEncoding] == StringValues.Empty && // Not specific encoding already set
_shouldCompressResponse(_response.HttpContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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;
using System.IO.Compression;

namespace Microsoft.AspNetCore.ResponseCompression
{
/// <summary>
/// GZIP compression provider.
/// </summary>
public class GzipResponseCompressionProvider : IResponseCompressionProvider
{
private readonly CompressionLevel _level;

/// <summary>
/// Initialize a new <see cref="GzipResponseCompressionProvider"/>.
/// </summary>
/// <param name="level">The compression level.</param>
public GzipResponseCompressionProvider(CompressionLevel level)
{
_level = level;
}

/// <inheritdoc />
public string EncodingName { get; } = "gzip";

/// <inheritdoc />
public Stream CreateStream(Stream outputStream)
{
return new GZipStream(outputStream, _level, true);
}
}
}
Loading