HttpResultFactory.cs 29 KB

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