BaseHandler.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.IO;
  5. using System.IO.Compression;
  6. using System.Linq;
  7. using System.Net;
  8. using MediaBrowser.Common.Logging;
  9. namespace MediaBrowser.Common.Net.Handlers
  10. {
  11. public abstract class BaseHandler
  12. {
  13. private Stream CompressedStream { get; set; }
  14. public virtual bool? UseChunkedEncoding
  15. {
  16. get
  17. {
  18. return null;
  19. }
  20. }
  21. private bool _TotalContentLengthDiscovered = false;
  22. private long? _TotalContentLength = null;
  23. public long? TotalContentLength
  24. {
  25. get
  26. {
  27. if (!_TotalContentLengthDiscovered)
  28. {
  29. _TotalContentLength = GetTotalContentLength();
  30. }
  31. return _TotalContentLength;
  32. }
  33. }
  34. /// <summary>
  35. /// Returns true or false indicating if the handler writes to the stream asynchronously.
  36. /// If so the subclass will be responsible for disposing the stream when complete.
  37. /// </summary>
  38. protected virtual bool IsAsyncHandler
  39. {
  40. get
  41. {
  42. return false;
  43. }
  44. }
  45. protected virtual bool SupportsByteRangeRequests
  46. {
  47. get
  48. {
  49. return false;
  50. }
  51. }
  52. /// <summary>
  53. /// The original HttpListenerContext
  54. /// </summary>
  55. protected HttpListenerContext HttpListenerContext { get; set; }
  56. /// <summary>
  57. /// The original QueryString
  58. /// </summary>
  59. protected NameValueCollection QueryString
  60. {
  61. get
  62. {
  63. return HttpListenerContext.Request.QueryString;
  64. }
  65. }
  66. protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
  67. protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
  68. {
  69. get
  70. {
  71. if (_RequestedRanges == null)
  72. {
  73. _RequestedRanges = new List<KeyValuePair<long, long?>>();
  74. if (IsRangeRequest)
  75. {
  76. // Example: bytes=0-,32-63
  77. string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
  78. foreach (string range in ranges)
  79. {
  80. string[] vals = range.Split('-');
  81. long start = 0;
  82. long? end = null;
  83. if (!string.IsNullOrEmpty(vals[0]))
  84. {
  85. start = long.Parse(vals[0]);
  86. }
  87. if (!string.IsNullOrEmpty(vals[1]))
  88. {
  89. end = long.Parse(vals[1]);
  90. }
  91. _RequestedRanges.Add(new KeyValuePair<long, long?>(start, end));
  92. }
  93. }
  94. }
  95. return _RequestedRanges;
  96. }
  97. }
  98. protected bool IsRangeRequest
  99. {
  100. get
  101. {
  102. return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
  103. }
  104. }
  105. /// <summary>
  106. /// Gets the MIME type to include in the response headers
  107. /// </summary>
  108. public abstract string ContentType { get; }
  109. /// <summary>
  110. /// Gets the status code to include in the response headers
  111. /// </summary>
  112. protected int StatusCode { get; set; }
  113. /// <summary>
  114. /// Gets the cache duration to include in the response headers
  115. /// </summary>
  116. public virtual TimeSpan CacheDuration
  117. {
  118. get
  119. {
  120. return TimeSpan.FromTicks(0);
  121. }
  122. }
  123. private bool _LastDateModifiedDiscovered = false;
  124. private DateTime? _LastDateModified = null;
  125. /// <summary>
  126. /// Gets the last date modified of the content being returned, if this can be determined.
  127. /// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
  128. /// </summary>
  129. public DateTime? LastDateModified
  130. {
  131. get
  132. {
  133. if (!_LastDateModifiedDiscovered)
  134. {
  135. _LastDateModified = GetLastDateModified();
  136. }
  137. return _LastDateModified;
  138. }
  139. }
  140. public virtual bool CompressResponse
  141. {
  142. get
  143. {
  144. return true;
  145. }
  146. }
  147. private bool ClientSupportsCompression
  148. {
  149. get
  150. {
  151. string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  152. return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
  153. }
  154. }
  155. private string CompressionMethod
  156. {
  157. get
  158. {
  159. string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  160. if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
  161. {
  162. return "deflate";
  163. }
  164. if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
  165. {
  166. return "gzip";
  167. }
  168. return null;
  169. }
  170. }
  171. public virtual void ProcessRequest(HttpListenerContext ctx)
  172. {
  173. HttpListenerContext = ctx;
  174. Logger.LogInfo("Http Server received request at: " + ctx.Request.Url.ToString());
  175. Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
  176. ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
  177. ctx.Response.KeepAlive = true;
  178. if (SupportsByteRangeRequests && IsRangeRequest)
  179. {
  180. ctx.Response.Headers["Accept-Ranges"] = "bytes";
  181. }
  182. // Set the initial status code
  183. // When serving a range request, we need to return status code 206 to indicate a partial response body
  184. StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
  185. ctx.Response.ContentType = ContentType;
  186. TimeSpan cacheDuration = CacheDuration;
  187. if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
  188. {
  189. DateTime ifModifiedSince;
  190. if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince))
  191. {
  192. // If the cache hasn't expired yet just return a 304
  193. if (IsCacheValid(ifModifiedSince, cacheDuration, LastDateModified))
  194. {
  195. StatusCode = 304;
  196. }
  197. }
  198. }
  199. if (StatusCode == 200 || StatusCode == 206)
  200. {
  201. ProcessUncachedResponse(ctx, cacheDuration);
  202. }
  203. else
  204. {
  205. ctx.Response.StatusCode = StatusCode;
  206. ctx.Response.SendChunked = false;
  207. DisposeResponseStream();
  208. }
  209. }
  210. private void ProcessUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
  211. {
  212. long? totalContentLength = TotalContentLength;
  213. // By default, use chunked encoding if we don't know the content length
  214. bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
  215. // Don't force this to true. HttpListener will default it to true if supported by the client.
  216. if (!useChunkedEncoding)
  217. {
  218. ctx.Response.SendChunked = false;
  219. }
  220. // Set the content length, if we know it
  221. if (totalContentLength.HasValue)
  222. {
  223. ctx.Response.ContentLength64 = totalContentLength.Value;
  224. }
  225. // Add the compression header
  226. if (CompressResponse && ClientSupportsCompression)
  227. {
  228. ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
  229. }
  230. // Add caching headers
  231. if (cacheDuration.Ticks > 0)
  232. {
  233. CacheResponse(ctx.Response, cacheDuration, LastDateModified);
  234. }
  235. PrepareUncachedResponse(ctx, cacheDuration);
  236. // Set the status code
  237. ctx.Response.StatusCode = StatusCode;
  238. if (StatusCode == 200 || StatusCode == 206)
  239. {
  240. // Finally, write the response data
  241. Stream outputStream = ctx.Response.OutputStream;
  242. if (CompressResponse && ClientSupportsCompression)
  243. {
  244. if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
  245. {
  246. CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
  247. }
  248. else
  249. {
  250. CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
  251. }
  252. outputStream = CompressedStream;
  253. }
  254. WriteResponseToOutputStream(outputStream);
  255. if (!IsAsyncHandler)
  256. {
  257. DisposeResponseStream();
  258. }
  259. }
  260. else
  261. {
  262. ctx.Response.SendChunked = false;
  263. DisposeResponseStream();
  264. }
  265. }
  266. protected virtual void PrepareUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
  267. {
  268. }
  269. private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified)
  270. {
  271. DateTime lastModified = dateModified ?? DateTime.Now;
  272. response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
  273. response.Headers[HttpResponseHeader.Expires] = DateTime.Now.Add(duration).ToString("r");
  274. response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
  275. }
  276. protected abstract void WriteResponseToOutputStream(Stream stream);
  277. protected void DisposeResponseStream()
  278. {
  279. if (CompressedStream != null)
  280. {
  281. CompressedStream.Dispose();
  282. }
  283. HttpListenerContext.Response.OutputStream.Dispose();
  284. }
  285. private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
  286. {
  287. if (dateModified.HasValue)
  288. {
  289. DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
  290. ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
  291. return lastModified <= ifModifiedSince;
  292. }
  293. DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
  294. if (DateTime.Now < cacheExpirationDate)
  295. {
  296. return true;
  297. }
  298. return false;
  299. }
  300. /// <summary>
  301. /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
  302. /// </summary>
  303. private DateTime NormalizeDateForComparison(DateTime date)
  304. {
  305. return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
  306. }
  307. protected virtual long? GetTotalContentLength()
  308. {
  309. return null;
  310. }
  311. protected virtual DateTime? GetLastDateModified()
  312. {
  313. return null;
  314. }
  315. }
  316. }