-
Notifications
You must be signed in to change notification settings - Fork 10.8k
Expand file tree
/
Copy pathStaticFileContext.cs
More file actions
404 lines (358 loc) · 14.9 KB
/
Copy pathStaticFileContext.cs
File metadata and controls
404 lines (358 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// 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;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.StaticFiles
{
internal struct StaticFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;
private readonly HttpContext _context;
private readonly StaticFileOptions _options;
private readonly PathString _matchUrl;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
private string _method;
private bool _isGet;
private bool _isHead;
private PathString _subPath;
private string _contentType;
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
private RangeItemHeaderValue _range;
private bool _isRangeRequest;
public StaticFileContext(HttpContext context, StaticFileOptions options, PathString matchUrl, ILogger logger, IFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
{
_context = context;
_options = options;
_matchUrl = matchUrl;
_request = context.Request;
_response = context.Response;
_logger = logger;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider;
_contentTypeProvider = contentTypeProvider;
_method = null;
_isGet = false;
_isHead = false;
_subPath = PathString.Empty;
_contentType = null;
_fileInfo = null;
_length = 0;
_lastModified = new DateTimeOffset();
_etag = null;
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
_range = null;
_isRangeRequest = false;
}
internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
public bool IsHeadMethod
{
get { return _isHead; }
}
public bool IsRangeRequest
{
get { return _isRangeRequest; }
}
public string SubPath
{
get { return _subPath.Value; }
}
public string PhysicalPath
{
get { return _fileInfo?.PhysicalPath; }
}
public bool ValidateMethod()
{
_method = _request.Method;
_isGet = HttpMethods.IsGet(_method);
_isHead = HttpMethods.IsHead(_method);
return _isGet || _isHead;
}
// Check if the URL matches any expected paths
public bool ValidatePath()
{
return Helpers.TryMatchPath(_context, _matchUrl, forDirectory: false, subpath: out _subPath);
}
public bool LookupContentType()
{
if (_contentTypeProvider.TryGetContentType(_subPath.Value, out _contentType))
{
return true;
}
if (_options.ServeUnknownFileTypes)
{
_contentType = _options.DefaultContentType;
return true;
}
return false;
}
public bool LookupFileInfo()
{
_fileInfo = _fileProvider.GetFileInfo(_subPath.Value);
if (_fileInfo.Exists)
{
_length = _fileInfo.Length;
DateTimeOffset last = _fileInfo.LastModified;
// Truncate to the second.
_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
long etagHash = _lastModified.ToFileTime() ^ _length;
_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}
return _fileInfo.Exists;
}
public void ComprehendRequestHeaders()
{
ComputeIfMatch();
ComputeIfModifiedSince();
ComputeRange();
ComputeIfRange();
}
private void ComputeIfMatch()
{
// 14.24 If-Match
var ifMatch = _requestHeaders.IfMatch;
if (ifMatch != null && ifMatch.Any())
{
_ifMatchState = PreconditionState.PreconditionFailed;
foreach (var etag in ifMatch)
{
if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true))
{
_ifMatchState = PreconditionState.ShouldProcess;
break;
}
}
}
// 14.26 If-None-Match
var ifNoneMatch = _requestHeaders.IfNoneMatch;
if (ifNoneMatch != null && ifNoneMatch.Any())
{
_ifNoneMatchState = PreconditionState.ShouldProcess;
foreach (var etag in ifNoneMatch)
{
if (etag.Equals(EntityTagHeaderValue.Any) || etag.Compare(_etag, useStrongComparison: true))
{
_ifNoneMatchState = PreconditionState.NotModified;
break;
}
}
}
}
private void ComputeIfModifiedSince()
{
var now = DateTimeOffset.UtcNow;
// 14.25 If-Modified-Since
var ifModifiedSince = _requestHeaders.IfModifiedSince;
if (ifModifiedSince.HasValue && ifModifiedSince <= now)
{
bool modified = ifModifiedSince < _lastModified;
_ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
}
// 14.28 If-Unmodified-Since
var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
{
bool unmodified = ifUnmodifiedSince >= _lastModified;
_ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
}
}
private void ComputeIfRange()
{
// 14.27 If-Range
var ifRangeHeader = _requestHeaders.IfRange;
if (ifRangeHeader != null)
{
// If the validator given in the If-Range header field matches the
// current validator for the selected representation of the target
// resource, then the server SHOULD process the Range header field as
// requested. If the validator does not match, the server MUST ignore
// the Range header field.
if (ifRangeHeader.LastModified.HasValue)
{
if (_lastModified !=null && _lastModified > ifRangeHeader.LastModified)
{
_isRangeRequest = false;
}
}
else if (_etag != null && ifRangeHeader.EntityTag != null && !ifRangeHeader.EntityTag.Compare(_etag, useStrongComparison: true))
{
_isRangeRequest = false;
}
}
}
private void ComputeRange()
{
// 14.35 Range
// http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-24
// A server MUST ignore a Range header field received with a request method other
// than GET.
if (!_isGet)
{
return;
}
(_isRangeRequest, _range) = RangeHelper.ParseRange(_context, _requestHeaders, _length, _logger);
}
public void ApplyResponseHeaders(int statusCode)
{
_response.StatusCode = statusCode;
if (statusCode < 400)
{
// these headers are returned for 200, 206, and 304
// they are not returned for 412 and 416
if (!string.IsNullOrEmpty(_contentType))
{
_response.ContentType = _contentType;
}
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
// this header is only returned here for 200
// it already set to the returned range for 206
// it is not returned for 304, 412, and 416
_response.ContentLength = _length;
}
_options.OnPrepareResponse(new StaticFileResponseContext
{
Context = _context,
File = _fileInfo,
});
}
public PreconditionState GetPreconditionState()
{
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState,
_ifModifiedSinceState, _ifUnmodifiedSinceState);
}
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
{
PreconditionState max = PreconditionState.Unspecified;
for (int i = 0; i < states.Length; i++)
{
if (states[i] > max)
{
max = states[i];
}
}
return max;
}
public Task SendStatusAsync(int statusCode)
{
ApplyResponseHeaders(statusCode);
_logger.LogHandled(statusCode, SubPath);
return Task.CompletedTask;
}
public async Task SendAsync()
{
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
// We don't need to directly cancel this, if the client disconnects it will fail silently.
await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
return;
}
try
{
using (var readStream = _fileInfo.CreateReadStream())
{
// Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
}
}
catch (OperationCanceledException ex)
{
_logger.LogWriteCancelled(ex);
// Don't throw this exception, it's most likely caused by the client disconnecting.
// However, if it was cancelled for any other reason we need to prevent empty responses.
_context.Abort();
}
}
// When there is only a single range the bytes are sent directly in the body.
internal async Task SendRangeAsync()
{
if (_range == null)
{
// 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
// SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
// the current length of the selected resource. e.g. */length
_responseHeaders.ContentRange = new ContentRangeHeaderValue(_length);
ApplyResponseHeaders(Constants.Status416RangeNotSatisfiable);
_logger.LogRangeNotSatisfiable(SubPath);
return;
}
_responseHeaders.ContentRange = ComputeContentRange(_range, out var start, out var length);
_response.ContentLength = length;
ApplyResponseHeaders(Constants.Status206PartialContent);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
_logger.LogSendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);
// We don't need to directly cancel this, if the client disconnects it will fail silently.
await sendFile.SendFileAsync(physicalPath, start, length, CancellationToken.None);
return;
}
try
{
using (var readStream = _fileInfo.CreateReadStream())
{
readStream.Seek(start, SeekOrigin.Begin); // TODO: What if !CanSeek?
_logger.LogCopyingFileRange(_response.Headers[HeaderNames.ContentRange], SubPath);
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, length, _context.RequestAborted);
}
}
catch (OperationCanceledException ex)
{
_logger.LogWriteCancelled(ex);
// Don't throw this exception, it's most likely caused by the client disconnecting.
// However, if it was cancelled for any other reason we need to prevent empty responses.
_context.Abort();
}
}
// Note: This assumes ranges have been normalized to absolute byte offsets.
private ContentRangeHeaderValue ComputeContentRange(RangeItemHeaderValue range, out long start, out long length)
{
start = range.From.Value;
long end = range.To.Value;
length = end - start + 1;
return new ContentRangeHeaderValue(start, end, _length);
}
}
}