1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- // Based on https://github.com/RickStrahl/Westwind.AspnetCore.LiveReload/blob/128b5f524e86954e997f2c453e7e5c1dcc3db746/Westwind.AspnetCore.LiveReload/ResponseStreamWrapper.cs
5
-
4
+ using System . Diagnostics ;
5
+ using System . IO . Compression ;
6
+ using System . IO . Pipelines ;
6
7
using Microsoft . AspNetCore . Http ;
7
8
using Microsoft . Extensions . Logging ;
8
9
using Microsoft . Net . Http . Headers ;
@@ -15,12 +16,18 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
15
16
/// </summary>
16
17
public class ResponseStreamWrapper : Stream
17
18
{
18
- private static readonly MediaTypeHeaderValue _textHtmlMediaType = new ( "text/html" ) ;
19
- private readonly Stream _baseStream ;
19
+ private static readonly MediaTypeHeaderValue s_textHtmlMediaType = new ( "text/html" ) ;
20
+
20
21
private readonly HttpContext _context ;
21
22
private readonly ILogger _logger ;
22
23
private bool ? _isHtmlResponse ;
23
24
25
+ private Stream _baseStream ;
26
+ private ScriptInjectingStream ? _scriptInjectingStream ;
27
+ private Pipe ? _pipe ;
28
+ private Task ? _gzipCopyTask ;
29
+ private bool _disposed ;
30
+
24
31
public ResponseStreamWrapper ( HttpContext context , ILogger logger )
25
32
{
26
33
_context = context ;
@@ -33,33 +40,25 @@ public ResponseStreamWrapper(HttpContext context, ILogger logger)
33
40
public override bool CanWrite => true ;
34
41
public override long Length { get ; }
35
42
public override long Position { get ; set ; }
36
- public bool ScriptInjectionPerformed { get ; private set ; }
37
-
38
- public bool IsHtmlResponse => _isHtmlResponse ?? false ;
43
+ public bool ScriptInjectionPerformed => _scriptInjectingStream ? . ScriptInjectionPerformed == true ;
44
+ public bool IsHtmlResponse => _isHtmlResponse == true ;
39
45
40
46
public override void Flush ( )
41
47
{
42
48
OnWrite ( ) ;
43
49
_baseStream . Flush ( ) ;
44
50
}
45
51
46
- public override Task FlushAsync ( CancellationToken cancellationToken )
52
+ public override async Task FlushAsync ( CancellationToken cancellationToken )
47
53
{
48
54
OnWrite ( ) ;
49
- return _baseStream . FlushAsync ( cancellationToken ) ;
55
+ await _baseStream . FlushAsync ( cancellationToken ) ;
50
56
}
51
57
52
58
public override void Write ( ReadOnlySpan < byte > buffer )
53
59
{
54
60
OnWrite ( ) ;
55
- if ( IsHtmlResponse && ! ScriptInjectionPerformed )
56
- {
57
- ScriptInjectionPerformed = WebSocketScriptInjection . TryInjectLiveReloadScript ( _baseStream , buffer ) ;
58
- }
59
- else
60
- {
61
- _baseStream . Write ( buffer ) ;
62
- }
61
+ _baseStream . Write ( buffer ) ;
63
62
}
64
63
65
64
public override void WriteByte ( byte value )
@@ -71,43 +70,19 @@ public override void WriteByte(byte value)
71
70
public override void Write ( byte [ ] buffer , int offset , int count )
72
71
{
73
72
OnWrite ( ) ;
74
-
75
- if ( IsHtmlResponse && ! ScriptInjectionPerformed )
76
- {
77
- ScriptInjectionPerformed = WebSocketScriptInjection . TryInjectLiveReloadScript ( _baseStream , buffer . AsSpan ( offset , count ) ) ;
78
- }
79
- else
80
- {
81
- _baseStream . Write ( buffer , offset , count ) ;
82
- }
73
+ _baseStream . Write ( buffer . AsSpan ( offset , count ) ) ;
83
74
}
84
75
85
76
public override async Task WriteAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken )
86
77
{
87
78
OnWrite ( ) ;
88
-
89
- if ( IsHtmlResponse && ! ScriptInjectionPerformed )
90
- {
91
- ScriptInjectionPerformed = await WebSocketScriptInjection . TryInjectLiveReloadScriptAsync ( _baseStream , buffer . AsMemory ( offset , count ) , cancellationToken ) ;
92
- }
93
- else
94
- {
95
- await _baseStream . WriteAsync ( buffer , offset , count , cancellationToken ) ;
96
- }
79
+ await _baseStream . WriteAsync ( buffer . AsMemory ( offset , count ) , cancellationToken ) ;
97
80
}
98
81
99
82
public override async ValueTask WriteAsync ( ReadOnlyMemory < byte > buffer , CancellationToken cancellationToken = default )
100
83
{
101
84
OnWrite ( ) ;
102
-
103
- if ( IsHtmlResponse && ! ScriptInjectionPerformed )
104
- {
105
- ScriptInjectionPerformed = await WebSocketScriptInjection . TryInjectLiveReloadScriptAsync ( _baseStream , buffer , cancellationToken ) ;
106
- }
107
- else
108
- {
109
- await _baseStream . WriteAsync ( buffer , cancellationToken ) ;
110
- }
85
+ await _baseStream . WriteAsync ( buffer , cancellationToken ) ;
111
86
}
112
87
113
88
private void OnWrite ( )
@@ -122,15 +97,38 @@ private void OnWrite()
122
97
_isHtmlResponse =
123
98
( response . StatusCode == StatusCodes . Status200OK || response . StatusCode == StatusCodes . Status500InternalServerError ) &&
124
99
MediaTypeHeaderValue . TryParse ( response . ContentType , out var mediaType ) &&
125
- mediaType . IsSubsetOf ( _textHtmlMediaType ) &&
100
+ mediaType . IsSubsetOf ( s_textHtmlMediaType ) &&
126
101
( ! mediaType . Charset . HasValue || mediaType . Charset . Equals ( "utf-8" , StringComparison . OrdinalIgnoreCase ) ) ;
127
102
128
103
if ( _isHtmlResponse . Value )
129
104
{
130
105
BrowserRefreshMiddleware . Log . SetupResponseForBrowserRefresh ( _logger ) ;
131
-
132
106
// Since we're changing the markup content, reset the content-length
133
107
response . Headers . ContentLength = null ;
108
+
109
+ _scriptInjectingStream = new ScriptInjectingStream ( _baseStream ) ;
110
+
111
+ // By default, write directly to the script injection stream.
112
+ // We may change the base stream below if we detect that the response
113
+ // is compressed.
114
+ _baseStream = _scriptInjectingStream ;
115
+
116
+ // Check if the response has gzip Content-Encoding
117
+ if ( response . Headers . TryGetValue ( HeaderNames . ContentEncoding , out var contentEncodingValues ) )
118
+ {
119
+ var contentEncoding = contentEncodingValues . FirstOrDefault ( ) ;
120
+ if ( string . Equals ( contentEncoding , "gzip" , StringComparison . OrdinalIgnoreCase ) )
121
+ {
122
+ // Remove the Content-Encoding header since we'll be serving uncompressed content
123
+ response . Headers . Remove ( HeaderNames . ContentEncoding ) ;
124
+
125
+ _pipe = new Pipe ( ) ;
126
+ var gzipStream = new GZipStream ( _pipe . Reader . AsStream ( leaveOpen : true ) , CompressionMode . Decompress , leaveOpen : true ) ;
127
+
128
+ _gzipCopyTask = gzipStream . CopyToAsync ( _scriptInjectingStream ) ;
129
+ _baseStream = _pipe . Writer . AsStream ( leaveOpen : true ) ;
130
+ }
131
+ }
134
132
}
135
133
}
136
134
@@ -145,5 +143,45 @@ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken
145
143
=> throw new NotSupportedException ( ) ;
146
144
147
145
public override void SetLength ( long value ) => throw new NotSupportedException ( ) ;
146
+
147
+ protected override void Dispose ( bool disposing )
148
+ {
149
+ if ( disposing )
150
+ {
151
+ DisposeAsync ( ) . AsTask ( ) . GetAwaiter ( ) . GetResult ( ) ;
152
+ }
153
+ }
154
+
155
+ public ValueTask CompleteAsync ( ) => DisposeAsync ( ) ;
156
+
157
+ public override async ValueTask DisposeAsync ( )
158
+ {
159
+ if ( _disposed )
160
+ {
161
+ return ;
162
+ }
163
+
164
+ _disposed = true ;
165
+
166
+ if ( _pipe is not null )
167
+ {
168
+ await _pipe . Writer . CompleteAsync ( ) ;
169
+ }
170
+
171
+ if ( _gzipCopyTask is not null )
172
+ {
173
+ await _gzipCopyTask ;
174
+ }
175
+
176
+ if ( _scriptInjectingStream is not null )
177
+ {
178
+ await _scriptInjectingStream . CompleteAsync ( ) ;
179
+ }
180
+ else
181
+ {
182
+ Debug . Assert ( _isHtmlResponse != true ) ;
183
+ await _baseStream . FlushAsync ( ) ;
184
+ }
185
+ }
148
186
}
149
187
}
0 commit comments