BaseHandler.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. using MediaBrowser.Common.Logging;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.Collections.Specialized;
  6. using System.IO;
  7. using System.IO.Compression;
  8. using System.Linq;
  9. using System.Net;
  10. using System.Threading.Tasks;
  11. namespace MediaBrowser.Common.Net.Handlers
  12. {
  13. public abstract class BaseHandler
  14. {
  15. public abstract bool HandlesRequest(HttpListenerRequest request);
  16. private Stream CompressedStream { get; set; }
  17. public virtual bool? UseChunkedEncoding
  18. {
  19. get
  20. {
  21. return null;
  22. }
  23. }
  24. private bool _TotalContentLengthDiscovered = false;
  25. private long? _TotalContentLength = null;
  26. public long? TotalContentLength
  27. {
  28. get
  29. {
  30. if (!_TotalContentLengthDiscovered)
  31. {
  32. _TotalContentLength = GetTotalContentLength();
  33. }
  34. return _TotalContentLength;
  35. }
  36. }
  37. protected virtual bool SupportsByteRangeRequests
  38. {
  39. get
  40. {
  41. return false;
  42. }
  43. }
  44. /// <summary>
  45. /// The original HttpListenerContext
  46. /// </summary>
  47. protected HttpListenerContext HttpListenerContext { get; set; }
  48. /// <summary>
  49. /// The original QueryString
  50. /// </summary>
  51. protected NameValueCollection QueryString
  52. {
  53. get
  54. {
  55. return HttpListenerContext.Request.QueryString;
  56. }
  57. }
  58. protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
  59. protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
  60. {
  61. get
  62. {
  63. if (_RequestedRanges == null)
  64. {
  65. _RequestedRanges = new List<KeyValuePair<long, long?>>();
  66. if (IsRangeRequest)
  67. {
  68. // Example: bytes=0-,32-63
  69. string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
  70. foreach (string range in ranges)
  71. {
  72. string[] vals = range.Split('-');
  73. long start = 0;
  74. long? end = null;
  75. if (!string.IsNullOrEmpty(vals[0]))
  76. {
  77. start = long.Parse(vals[0]);
  78. }
  79. if (!string.IsNullOrEmpty(vals[1]))
  80. {
  81. end = long.Parse(vals[1]);
  82. }
  83. _RequestedRanges.Add(new KeyValuePair<long, long?>(start, end));
  84. }
  85. }
  86. }
  87. return _RequestedRanges;
  88. }
  89. }
  90. protected bool IsRangeRequest
  91. {
  92. get
  93. {
  94. return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
  95. }
  96. }
  97. /// <summary>
  98. /// Gets the MIME type to include in the response headers
  99. /// </summary>
  100. public abstract Task<string> GetContentType();
  101. /// <summary>
  102. /// Gets the status code to include in the response headers
  103. /// </summary>
  104. protected int StatusCode { get; set; }
  105. /// <summary>
  106. /// Gets the cache duration to include in the response headers
  107. /// </summary>
  108. public virtual TimeSpan CacheDuration
  109. {
  110. get
  111. {
  112. return TimeSpan.FromTicks(0);
  113. }
  114. }
  115. public virtual bool ShouldCompressResponse(string contentType)
  116. {
  117. return true;
  118. }
  119. private bool ClientSupportsCompression
  120. {
  121. get
  122. {
  123. string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  124. return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
  125. }
  126. }
  127. private string CompressionMethod
  128. {
  129. get
  130. {
  131. string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  132. if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
  133. {
  134. return "deflate";
  135. }
  136. if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
  137. {
  138. return "gzip";
  139. }
  140. return null;
  141. }
  142. }
  143. public virtual async Task ProcessRequest(HttpListenerContext ctx)
  144. {
  145. HttpListenerContext = ctx;
  146. string url = ctx.Request.Url.ToString();
  147. Logger.LogInfo("Http Server received request at: " + url);
  148. Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
  149. ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
  150. ctx.Response.KeepAlive = true;
  151. try
  152. {
  153. if (SupportsByteRangeRequests && IsRangeRequest)
  154. {
  155. ctx.Response.Headers["Accept-Ranges"] = "bytes";
  156. }
  157. // Set the initial status code
  158. // When serving a range request, we need to return status code 206 to indicate a partial response body
  159. StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
  160. ctx.Response.ContentType = await GetContentType().ConfigureAwait(false);
  161. TimeSpan cacheDuration = CacheDuration;
  162. DateTime? lastDateModified = await GetLastDateModified().ConfigureAwait(false);
  163. if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
  164. {
  165. DateTime ifModifiedSince;
  166. if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
  167. {
  168. // If the cache hasn't expired yet just return a 304
  169. if (IsCacheValid(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified))
  170. {
  171. StatusCode = 304;
  172. }
  173. }
  174. }
  175. await PrepareResponse().ConfigureAwait(false);
  176. Logger.LogInfo("Responding with status code {0} for url {1}", StatusCode, url);
  177. if (IsResponseValid)
  178. {
  179. bool compressResponse = ShouldCompressResponse(ctx.Response.ContentType) && ClientSupportsCompression;
  180. await ProcessUncachedRequest(ctx, compressResponse, cacheDuration, lastDateModified).ConfigureAwait(false);
  181. }
  182. else
  183. {
  184. ctx.Response.StatusCode = StatusCode;
  185. ctx.Response.SendChunked = false;
  186. }
  187. }
  188. catch (Exception ex)
  189. {
  190. // It might be too late if some response data has already been transmitted, but try to set this
  191. ctx.Response.StatusCode = 500;
  192. Logger.LogException(ex);
  193. }
  194. finally
  195. {
  196. DisposeResponseStream();
  197. }
  198. }
  199. private async Task ProcessUncachedRequest(HttpListenerContext ctx, bool compressResponse, TimeSpan cacheDuration, DateTime? lastDateModified)
  200. {
  201. long? totalContentLength = TotalContentLength;
  202. // By default, use chunked encoding if we don't know the content length
  203. bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
  204. // Don't force this to true. HttpListener will default it to true if supported by the client.
  205. if (!useChunkedEncoding)
  206. {
  207. ctx.Response.SendChunked = false;
  208. }
  209. // Set the content length, if we know it
  210. if (totalContentLength.HasValue)
  211. {
  212. ctx.Response.ContentLength64 = totalContentLength.Value;
  213. }
  214. // Add the compression header
  215. if (compressResponse)
  216. {
  217. ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
  218. }
  219. // Add caching headers
  220. if (cacheDuration.Ticks > 0)
  221. {
  222. CacheResponse(ctx.Response, cacheDuration, lastDateModified);
  223. }
  224. // Set the status code
  225. ctx.Response.StatusCode = StatusCode;
  226. if (IsResponseValid)
  227. {
  228. // Finally, write the response data
  229. Stream outputStream = ctx.Response.OutputStream;
  230. if (compressResponse)
  231. {
  232. if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
  233. {
  234. CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
  235. }
  236. else
  237. {
  238. CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
  239. }
  240. outputStream = CompressedStream;
  241. }
  242. await WriteResponseToOutputStream(outputStream).ConfigureAwait(false);
  243. }
  244. else
  245. {
  246. ctx.Response.SendChunked = false;
  247. }
  248. }
  249. private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified)
  250. {
  251. DateTime now = DateTime.UtcNow;
  252. DateTime lastModified = dateModified ?? now;
  253. response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
  254. response.Headers[HttpResponseHeader.Expires] = now.Add(duration).ToString("r");
  255. response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
  256. }
  257. /// <summary>
  258. /// Gives subclasses a chance to do any prep work, and also to validate data and set an error status code, if needed
  259. /// </summary>
  260. protected virtual Task PrepareResponse()
  261. {
  262. return Task.FromResult<object>(null);
  263. }
  264. protected abstract Task WriteResponseToOutputStream(Stream stream);
  265. protected virtual void DisposeResponseStream()
  266. {
  267. if (CompressedStream != null)
  268. {
  269. CompressedStream.Dispose();
  270. }
  271. HttpListenerContext.Response.OutputStream.Dispose();
  272. }
  273. private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
  274. {
  275. if (dateModified.HasValue)
  276. {
  277. DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
  278. ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
  279. return lastModified <= ifModifiedSince;
  280. }
  281. DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
  282. if (DateTime.UtcNow < cacheExpirationDate)
  283. {
  284. return true;
  285. }
  286. return false;
  287. }
  288. /// <summary>
  289. /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
  290. /// </summary>
  291. private DateTime NormalizeDateForComparison(DateTime date)
  292. {
  293. return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
  294. }
  295. protected virtual long? GetTotalContentLength()
  296. {
  297. return null;
  298. }
  299. protected virtual Task<DateTime?> GetLastDateModified()
  300. {
  301. DateTime? value = null;
  302. return Task.FromResult<DateTime?>(value);
  303. }
  304. private bool IsResponseValid
  305. {
  306. get
  307. {
  308. return StatusCode == 200 || StatusCode == 206;
  309. }
  310. }
  311. private Hashtable _FormValues = null;
  312. /// <summary>
  313. /// Gets a value from form POST data
  314. /// </summary>
  315. protected async Task<string> GetFormValue(string name)
  316. {
  317. if (_FormValues == null)
  318. {
  319. _FormValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
  320. }
  321. if (_FormValues.ContainsKey(name))
  322. {
  323. return _FormValues[name].ToString();
  324. }
  325. return null;
  326. }
  327. /// <summary>
  328. /// Extracts form POST data from a request
  329. /// </summary>
  330. private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
  331. {
  332. Hashtable formVars = new Hashtable();
  333. if (request.HasEntityBody)
  334. {
  335. if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
  336. {
  337. using (Stream requestBody = request.InputStream)
  338. {
  339. using (StreamReader reader = new StreamReader(requestBody, request.ContentEncoding))
  340. {
  341. string s = await reader.ReadToEndAsync().ConfigureAwait(false);
  342. string[] pairs = s.Split('&');
  343. for (int x = 0; x < pairs.Length; x++)
  344. {
  345. string pair = pairs[x];
  346. int index = pair.IndexOf('=');
  347. if (index != -1)
  348. {
  349. string name = pair.Substring(0, index);
  350. string value = pair.Substring(index + 1);
  351. formVars.Add(name, value);
  352. }
  353. }
  354. }
  355. }
  356. }
  357. }
  358. return formVars;
  359. }
  360. }
  361. }