HttpResultFactory.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. #pragma warning disable CS1591
  2. #pragma warning disable SA1600
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.IO.Compression;
  8. using System.Net;
  9. using System.Runtime.Serialization;
  10. using System.Text;
  11. using System.Threading.Tasks;
  12. using System.Xml;
  13. using Emby.Server.Implementations.Services;
  14. using MediaBrowser.Controller.Net;
  15. using MediaBrowser.Model.IO;
  16. using MediaBrowser.Model.Serialization;
  17. using MediaBrowser.Model.Services;
  18. using Microsoft.Extensions.Logging;
  19. using Microsoft.Extensions.Primitives;
  20. using Microsoft.Net.Http.Headers;
  21. using IRequest = MediaBrowser.Model.Services.IRequest;
  22. using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
  23. namespace Emby.Server.Implementations.HttpServer
  24. {
  25. /// <summary>
  26. /// Class HttpResultFactory.
  27. /// </summary>
  28. public class HttpResultFactory : IHttpResultFactory
  29. {
  30. // Last-Modified and If-Modified-Since must follow strict date format,
  31. // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
  32. private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
  33. // We specifically use en-US culture because both day of week and month names require it
  34. private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
  35. /// <summary>
  36. /// The logger.
  37. /// </summary>
  38. private readonly ILogger _logger;
  39. private readonly IFileSystem _fileSystem;
  40. private readonly IJsonSerializer _jsonSerializer;
  41. private readonly IStreamHelper _streamHelper;
  42. /// <summary>
  43. /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
  44. /// </summary>
  45. public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
  46. {
  47. _fileSystem = fileSystem;
  48. _jsonSerializer = jsonSerializer;
  49. _streamHelper = streamHelper;
  50. _logger = loggerfactory.CreateLogger("HttpResultFactory");
  51. }
  52. /// <summary>
  53. /// Gets the result.
  54. /// </summary>
  55. /// <param name="content">The content.</param>
  56. /// <param name="contentType">Type of the content.</param>
  57. /// <param name="responseHeaders">The response headers.</param>
  58. /// <returns>System.Object.</returns>
  59. public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
  60. {
  61. return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
  62. }
  63. public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
  64. {
  65. return GetHttpResult(null, content, contentType, true, responseHeaders);
  66. }
  67. public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
  68. {
  69. return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
  70. }
  71. public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
  72. {
  73. return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
  74. }
  75. public object GetRedirectResult(string url)
  76. {
  77. var responseHeaders = new Dictionary<string, string>();
  78. responseHeaders[HeaderNames.Location] = url;
  79. var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
  80. AddResponseHeaders(result, responseHeaders);
  81. return result;
  82. }
  83. /// <summary>
  84. /// Gets the HTTP result.
  85. /// </summary>
  86. private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
  87. {
  88. var result = new StreamWriter(content, contentType);
  89. if (responseHeaders == null)
  90. {
  91. responseHeaders = new Dictionary<string, string>();
  92. }
  93. if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
  94. {
  95. responseHeaders[HeaderNames.Expires] = "0";
  96. }
  97. AddResponseHeaders(result, responseHeaders);
  98. return result;
  99. }
  100. /// <summary>
  101. /// Gets the HTTP result.
  102. /// </summary>
  103. private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
  104. {
  105. string compressionType = null;
  106. bool isHeadRequest = false;
  107. if (requestContext != null)
  108. {
  109. compressionType = GetCompressionType(requestContext, content, contentType);
  110. isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
  111. }
  112. IHasHeaders result;
  113. if (string.IsNullOrEmpty(compressionType))
  114. {
  115. var contentLength = content.Length;
  116. if (isHeadRequest)
  117. {
  118. content = Array.Empty<byte>();
  119. }
  120. result = new StreamWriter(content, contentType, contentLength);
  121. }
  122. else
  123. {
  124. result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
  125. }
  126. if (responseHeaders == null)
  127. {
  128. responseHeaders = new Dictionary<string, string>();
  129. }
  130. if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
  131. {
  132. responseHeaders[HeaderNames.Expires] = "0";
  133. }
  134. AddResponseHeaders(result, responseHeaders);
  135. return result;
  136. }
  137. /// <summary>
  138. /// Gets the HTTP result.
  139. /// </summary>
  140. private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
  141. {
  142. IHasHeaders result;
  143. var bytes = Encoding.UTF8.GetBytes(content);
  144. var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
  145. var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
  146. if (string.IsNullOrEmpty(compressionType))
  147. {
  148. var contentLength = bytes.Length;
  149. if (isHeadRequest)
  150. {
  151. bytes = Array.Empty<byte>();
  152. }
  153. result = new StreamWriter(bytes, contentType, contentLength);
  154. }
  155. else
  156. {
  157. result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
  158. }
  159. if (responseHeaders == null)
  160. {
  161. responseHeaders = new Dictionary<string, string>();
  162. }
  163. if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
  164. {
  165. responseHeaders[HeaderNames.Expires] = "0";
  166. }
  167. AddResponseHeaders(result, responseHeaders);
  168. return result;
  169. }
  170. /// <summary>
  171. /// Gets the optimized result.
  172. /// </summary>
  173. /// <typeparam name="T"></typeparam>
  174. public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
  175. where T : class
  176. {
  177. if (result == null)
  178. {
  179. throw new ArgumentNullException(nameof(result));
  180. }
  181. if (responseHeaders == null)
  182. {
  183. responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  184. }
  185. responseHeaders[HeaderNames.Expires] = "0";
  186. return ToOptimizedResultInternal(requestContext, result, responseHeaders);
  187. }
  188. private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
  189. {
  190. if (responseContentType == null)
  191. {
  192. return null;
  193. }
  194. // Per apple docs, hls manifests must be compressed
  195. if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
  196. responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
  197. responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
  198. responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
  199. responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
  200. {
  201. return null;
  202. }
  203. if (content.Length < 1024)
  204. {
  205. return null;
  206. }
  207. return GetCompressionType(request);
  208. }
  209. private static string GetCompressionType(IRequest request)
  210. {
  211. var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
  212. if (string.IsNullOrEmpty(acceptEncoding))
  213. {
  214. //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
  215. // return "br";
  216. if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
  217. return "deflate";
  218. if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
  219. return "gzip";
  220. }
  221. return null;
  222. }
  223. /// <summary>
  224. /// Returns the optimized result for the IRequestContext.
  225. /// Does not use or store results in any cache.
  226. /// </summary>
  227. /// <param name="request"></param>
  228. /// <param name="dto"></param>
  229. /// <returns></returns>
  230. public object ToOptimizedResult<T>(IRequest request, T dto)
  231. {
  232. return ToOptimizedResultInternal(request, dto);
  233. }
  234. private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
  235. {
  236. // TODO: @bond use Span and .Equals
  237. var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
  238. switch (contentType)
  239. {
  240. case "application/xml":
  241. case "text/xml":
  242. case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
  243. return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
  244. case "application/json":
  245. case "text/json":
  246. return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
  247. default:
  248. break;
  249. }
  250. var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
  251. var ms = new MemoryStream();
  252. var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
  253. writerFn(dto, ms);
  254. ms.Position = 0;
  255. if (isHeadRequest)
  256. {
  257. using (ms)
  258. {
  259. return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
  260. }
  261. }
  262. return GetHttpResult(request, ms, contentType, true, responseHeaders);
  263. }
  264. private IHasHeaders GetCompressedResult(byte[] content,
  265. string requestedCompressionType,
  266. IDictionary<string, string> responseHeaders,
  267. bool isHeadRequest,
  268. string contentType)
  269. {
  270. if (responseHeaders == null)
  271. {
  272. responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  273. }
  274. content = Compress(content, requestedCompressionType);
  275. responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
  276. responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
  277. var contentLength = content.Length;
  278. if (isHeadRequest)
  279. {
  280. var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
  281. AddResponseHeaders(result, responseHeaders);
  282. return result;
  283. }
  284. else
  285. {
  286. var result = new StreamWriter(content, contentType, contentLength);
  287. AddResponseHeaders(result, responseHeaders);
  288. return result;
  289. }
  290. }
  291. private byte[] Compress(byte[] bytes, string compressionType)
  292. {
  293. if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
  294. {
  295. return Deflate(bytes);
  296. }
  297. if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
  298. {
  299. return GZip(bytes);
  300. }
  301. throw new NotSupportedException(compressionType);
  302. }
  303. private static byte[] Deflate(byte[] bytes)
  304. {
  305. // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
  306. // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
  307. using (var ms = new MemoryStream())
  308. using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
  309. {
  310. zipStream.Write(bytes, 0, bytes.Length);
  311. zipStream.Dispose();
  312. return ms.ToArray();
  313. }
  314. }
  315. private static byte[] GZip(byte[] buffer)
  316. {
  317. using (var ms = new MemoryStream())
  318. using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
  319. {
  320. zipStream.Write(buffer, 0, buffer.Length);
  321. zipStream.Dispose();
  322. return ms.ToArray();
  323. }
  324. }
  325. private static string SerializeToXmlString(object from)
  326. {
  327. using (var ms = new MemoryStream())
  328. {
  329. var xwSettings = new XmlWriterSettings();
  330. xwSettings.Encoding = new UTF8Encoding(false);
  331. xwSettings.OmitXmlDeclaration = false;
  332. using (var xw = XmlWriter.Create(ms, xwSettings))
  333. {
  334. var serializer = new DataContractSerializer(from.GetType());
  335. serializer.WriteObject(xw, from);
  336. xw.Flush();
  337. ms.Seek(0, SeekOrigin.Begin);
  338. using (var reader = new StreamReader(ms))
  339. {
  340. return reader.ReadToEnd();
  341. }
  342. }
  343. }
  344. }
  345. /// <summary>
  346. /// Pres the process optimized result.
  347. /// </summary>
  348. private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
  349. {
  350. bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
  351. AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
  352. if (!noCache)
  353. {
  354. if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader))
  355. {
  356. _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
  357. return null;
  358. }
  359. if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
  360. {
  361. AddAgeHeader(responseHeaders, options.DateLastModified);
  362. var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
  363. AddResponseHeaders(result, responseHeaders);
  364. return result;
  365. }
  366. }
  367. return null;
  368. }
  369. public Task<object> GetStaticFileResult(IRequest requestContext,
  370. string path,
  371. FileShare fileShare = FileShare.Read)
  372. {
  373. if (string.IsNullOrEmpty(path))
  374. {
  375. throw new ArgumentNullException(nameof(path));
  376. }
  377. return GetStaticFileResult(requestContext, new StaticFileResultOptions
  378. {
  379. Path = path,
  380. FileShare = fileShare
  381. });
  382. }
  383. public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
  384. {
  385. var path = options.Path;
  386. var fileShare = options.FileShare;
  387. if (string.IsNullOrEmpty(path))
  388. {
  389. throw new ArgumentException("Path can't be empty.", nameof(options));
  390. }
  391. if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
  392. {
  393. throw new ArgumentException("FileShare must be either Read or ReadWrite");
  394. }
  395. if (string.IsNullOrEmpty(options.ContentType))
  396. {
  397. options.ContentType = MimeTypes.GetMimeType(path);
  398. }
  399. if (!options.DateLastModified.HasValue)
  400. {
  401. options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
  402. }
  403. options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
  404. options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  405. return GetStaticResult(requestContext, options);
  406. }
  407. /// <summary>
  408. /// Gets the file stream.
  409. /// </summary>
  410. /// <param name="path">The path.</param>
  411. /// <param name="fileShare">The file share.</param>
  412. /// <returns>Stream.</returns>
  413. private Stream GetFileStream(string path, FileShare fileShare)
  414. {
  415. return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
  416. }
  417. public Task<object> GetStaticResult(IRequest requestContext,
  418. Guid cacheKey,
  419. DateTime? lastDateModified,
  420. TimeSpan? cacheDuration,
  421. string contentType,
  422. Func<Task<Stream>> factoryFn,
  423. IDictionary<string, string> responseHeaders = null,
  424. bool isHeadRequest = false)
  425. {
  426. return GetStaticResult(requestContext, new StaticResultOptions
  427. {
  428. CacheDuration = cacheDuration,
  429. ContentFactory = factoryFn,
  430. ContentType = contentType,
  431. DateLastModified = lastDateModified,
  432. IsHeadRequest = isHeadRequest,
  433. ResponseHeaders = responseHeaders
  434. });
  435. }
  436. public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
  437. {
  438. options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  439. var contentType = options.ContentType;
  440. if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
  441. {
  442. // See if the result is already cached in the browser
  443. var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
  444. if (result != null)
  445. {
  446. return result;
  447. }
  448. }
  449. // TODO: We don't really need the option value
  450. var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
  451. var factoryFn = options.ContentFactory;
  452. var responseHeaders = options.ResponseHeaders;
  453. AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
  454. AddAgeHeader(responseHeaders, options.DateLastModified);
  455. var rangeHeader = requestContext.Headers[HeaderNames.Range];
  456. if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
  457. {
  458. var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
  459. {
  460. OnComplete = options.OnComplete,
  461. OnError = options.OnError,
  462. FileShare = options.FileShare
  463. };
  464. AddResponseHeaders(hasHeaders, options.ResponseHeaders);
  465. return hasHeaders;
  466. }
  467. var stream = await factoryFn().ConfigureAwait(false);
  468. var totalContentLength = options.ContentLength;
  469. if (!totalContentLength.HasValue)
  470. {
  471. try
  472. {
  473. totalContentLength = stream.Length;
  474. }
  475. catch (NotSupportedException)
  476. {
  477. }
  478. }
  479. if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
  480. {
  481. var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger)
  482. {
  483. OnComplete = options.OnComplete
  484. };
  485. AddResponseHeaders(hasHeaders, options.ResponseHeaders);
  486. return hasHeaders;
  487. }
  488. else
  489. {
  490. if (totalContentLength.HasValue)
  491. {
  492. responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
  493. }
  494. if (isHeadRequest)
  495. {
  496. using (stream)
  497. {
  498. return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
  499. }
  500. }
  501. var hasHeaders = new StreamWriter(stream, contentType)
  502. {
  503. OnComplete = options.OnComplete,
  504. OnError = options.OnError
  505. };
  506. AddResponseHeaders(hasHeaders, options.ResponseHeaders);
  507. return hasHeaders;
  508. }
  509. }
  510. /// <summary>
  511. /// Adds the caching responseHeaders.
  512. /// </summary>
  513. private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
  514. bool noCache, DateTime? lastModifiedDate)
  515. {
  516. if (noCache)
  517. {
  518. responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
  519. responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
  520. return;
  521. }
  522. if (cacheDuration.HasValue)
  523. {
  524. responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
  525. }
  526. else
  527. {
  528. responseHeaders[HeaderNames.CacheControl] = "public";
  529. }
  530. if (lastModifiedDate.HasValue)
  531. {
  532. responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
  533. }
  534. }
  535. /// <summary>
  536. /// Adds the age header.
  537. /// </summary>
  538. /// <param name="responseHeaders">The responseHeaders.</param>
  539. /// <param name="lastDateModified">The last date modified.</param>
  540. private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
  541. {
  542. if (lastDateModified.HasValue)
  543. {
  544. responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
  545. }
  546. }
  547. /// <summary>
  548. /// Determines whether [is not modified] [the specified if modified since].
  549. /// </summary>
  550. /// <param name="ifModifiedSince">If modified since.</param>
  551. /// <param name="cacheDuration">Duration of the cache.</param>
  552. /// <param name="dateModified">The date modified.</param>
  553. /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
  554. private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
  555. {
  556. if (dateModified.HasValue)
  557. {
  558. var lastModified = NormalizeDateForComparison(dateModified.Value);
  559. ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
  560. return lastModified <= ifModifiedSince;
  561. }
  562. if (cacheDuration.HasValue)
  563. {
  564. var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
  565. if (DateTime.UtcNow < cacheExpirationDate)
  566. {
  567. return true;
  568. }
  569. }
  570. return false;
  571. }
  572. /// <summary>
  573. /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
  574. /// </summary>
  575. /// <param name="date">The date.</param>
  576. /// <returns>DateTime.</returns>
  577. private static DateTime NormalizeDateForComparison(DateTime date)
  578. {
  579. return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
  580. }
  581. /// <summary>
  582. /// Adds the response headers.
  583. /// </summary>
  584. /// <param name="hasHeaders">The has options.</param>
  585. /// <param name="responseHeaders">The response headers.</param>
  586. private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
  587. {
  588. foreach (var item in responseHeaders)
  589. {
  590. hasHeaders.Headers[item.Key] = item.Value;
  591. }
  592. }
  593. }
  594. }