2
0

BaseHandler.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Common.Kernel;
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Collections.Specialized;
  7. using System.Globalization;
  8. using System.IO;
  9. using System.IO.Compression;
  10. using System.Linq;
  11. using System.Net;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. using System.Web;
  15. namespace MediaBrowser.Common.Net.Handlers
  16. {
  17. /// <summary>
  18. /// Class BaseHandler
  19. /// </summary>
  20. public abstract class BaseHandler<TKernelType> : IHttpServerHandler
  21. where TKernelType : IKernel
  22. {
  23. /// <summary>
  24. /// Initializes the specified kernel.
  25. /// </summary>
  26. /// <param name="kernel">The kernel.</param>
  27. public void Initialize(IKernel kernel)
  28. {
  29. Kernel = (TKernelType)kernel;
  30. }
  31. /// <summary>
  32. /// Gets or sets the kernel.
  33. /// </summary>
  34. /// <value>The kernel.</value>
  35. protected TKernelType Kernel { get; private set; }
  36. /// <summary>
  37. /// Gets the URL suffix used to determine if this handler can process a request.
  38. /// </summary>
  39. /// <value>The URL suffix.</value>
  40. protected virtual string UrlSuffix
  41. {
  42. get
  43. {
  44. var name = GetType().Name;
  45. const string srch = "Handler";
  46. if (name.EndsWith(srch, StringComparison.OrdinalIgnoreCase))
  47. {
  48. name = name.Substring(0, name.Length - srch.Length);
  49. }
  50. return "api/" + name;
  51. }
  52. }
  53. /// <summary>
  54. /// Handleses the request.
  55. /// </summary>
  56. /// <param name="request">The request.</param>
  57. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  58. public virtual bool HandlesRequest(HttpListenerRequest request)
  59. {
  60. var name = '/' + UrlSuffix.TrimStart('/');
  61. var url = Kernel.WebApplicationName + name;
  62. return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
  63. }
  64. /// <summary>
  65. /// Gets or sets the compressed stream.
  66. /// </summary>
  67. /// <value>The compressed stream.</value>
  68. private Stream CompressedStream { get; set; }
  69. /// <summary>
  70. /// Gets a value indicating whether [use chunked encoding].
  71. /// </summary>
  72. /// <value><c>null</c> if [use chunked encoding] contains no value, <c>true</c> if [use chunked encoding]; otherwise, <c>false</c>.</value>
  73. public virtual bool? UseChunkedEncoding
  74. {
  75. get
  76. {
  77. return null;
  78. }
  79. }
  80. /// <summary>
  81. /// The original HttpListenerContext
  82. /// </summary>
  83. /// <value>The HTTP listener context.</value>
  84. protected HttpListenerContext HttpListenerContext { get; set; }
  85. /// <summary>
  86. /// The _query string
  87. /// </summary>
  88. private NameValueCollection _queryString;
  89. /// <summary>
  90. /// The original QueryString
  91. /// </summary>
  92. /// <value>The query string.</value>
  93. public NameValueCollection QueryString
  94. {
  95. get
  96. {
  97. // HttpListenerContext.Request.QueryString is not decoded properly
  98. return _queryString ?? (_queryString = HttpUtility.ParseQueryString(HttpListenerContext.Request.Url.Query));
  99. }
  100. }
  101. /// <summary>
  102. /// The _requested ranges
  103. /// </summary>
  104. private List<KeyValuePair<long, long?>> _requestedRanges;
  105. /// <summary>
  106. /// Gets the requested ranges.
  107. /// </summary>
  108. /// <value>The requested ranges.</value>
  109. protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
  110. {
  111. get
  112. {
  113. if (_requestedRanges == null)
  114. {
  115. _requestedRanges = new List<KeyValuePair<long, long?>>();
  116. if (IsRangeRequest)
  117. {
  118. // Example: bytes=0-,32-63
  119. var ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
  120. foreach (var range in ranges)
  121. {
  122. var vals = range.Split('-');
  123. long start = 0;
  124. long? end = null;
  125. if (!string.IsNullOrEmpty(vals[0]))
  126. {
  127. start = long.Parse(vals[0]);
  128. }
  129. if (!string.IsNullOrEmpty(vals[1]))
  130. {
  131. end = long.Parse(vals[1]);
  132. }
  133. _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
  134. }
  135. }
  136. }
  137. return _requestedRanges;
  138. }
  139. }
  140. /// <summary>
  141. /// Gets a value indicating whether this instance is range request.
  142. /// </summary>
  143. /// <value><c>true</c> if this instance is range request; otherwise, <c>false</c>.</value>
  144. protected bool IsRangeRequest
  145. {
  146. get
  147. {
  148. return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
  149. }
  150. }
  151. /// <summary>
  152. /// Gets a value indicating whether [client supports compression].
  153. /// </summary>
  154. /// <value><c>true</c> if [client supports compression]; otherwise, <c>false</c>.</value>
  155. protected bool ClientSupportsCompression
  156. {
  157. get
  158. {
  159. var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  160. return enc.Equals("*", StringComparison.OrdinalIgnoreCase) ||
  161. enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 ||
  162. enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
  163. }
  164. }
  165. /// <summary>
  166. /// Gets the compression method.
  167. /// </summary>
  168. /// <value>The compression method.</value>
  169. private string CompressionMethod
  170. {
  171. get
  172. {
  173. var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
  174. if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.Equals("*", StringComparison.OrdinalIgnoreCase))
  175. {
  176. return "deflate";
  177. }
  178. if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
  179. {
  180. return "gzip";
  181. }
  182. return null;
  183. }
  184. }
  185. /// <summary>
  186. /// Processes the request.
  187. /// </summary>
  188. /// <param name="ctx">The CTX.</param>
  189. /// <returns>Task.</returns>
  190. public virtual async Task ProcessRequest(HttpListenerContext ctx)
  191. {
  192. HttpListenerContext = ctx;
  193. ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
  194. ctx.Response.KeepAlive = true;
  195. try
  196. {
  197. await ProcessRequestInternal(ctx).ConfigureAwait(false);
  198. }
  199. catch (InvalidOperationException ex)
  200. {
  201. HandleException(ctx.Response, ex, 422);
  202. throw;
  203. }
  204. catch (ResourceNotFoundException ex)
  205. {
  206. HandleException(ctx.Response, ex, 404);
  207. throw;
  208. }
  209. catch (FileNotFoundException ex)
  210. {
  211. HandleException(ctx.Response, ex, 404);
  212. throw;
  213. }
  214. catch (DirectoryNotFoundException ex)
  215. {
  216. HandleException(ctx.Response, ex, 404);
  217. throw;
  218. }
  219. catch (UnauthorizedAccessException ex)
  220. {
  221. HandleException(ctx.Response, ex, 401);
  222. throw;
  223. }
  224. catch (ArgumentException ex)
  225. {
  226. HandleException(ctx.Response, ex, 400);
  227. throw;
  228. }
  229. catch (Exception ex)
  230. {
  231. HandleException(ctx.Response, ex, 500);
  232. throw;
  233. }
  234. finally
  235. {
  236. DisposeResponseStream();
  237. }
  238. }
  239. /// <summary>
  240. /// Appends the error message.
  241. /// </summary>
  242. /// <param name="response">The response.</param>
  243. /// <param name="ex">The ex.</param>
  244. /// <param name="statusCode">The status code.</param>
  245. private void HandleException(HttpListenerResponse response, Exception ex, int statusCode)
  246. {
  247. response.StatusCode = statusCode;
  248. response.Headers.Add("Status", statusCode.ToString(new CultureInfo("en-US")));
  249. response.Headers.Remove("Age");
  250. response.Headers.Remove("Expires");
  251. response.Headers.Remove("Cache-Control");
  252. response.Headers.Remove("Etag");
  253. response.Headers.Remove("Last-Modified");
  254. response.ContentType = "text/plain";
  255. //Logger.ErrorException("Error processing request", ex);
  256. if (!string.IsNullOrEmpty(ex.Message))
  257. {
  258. response.AddHeader("X-Application-Error-Code", ex.Message);
  259. }
  260. var bytes = Encoding.UTF8.GetBytes(ex.Message);
  261. var stream = CompressedStream ?? response.OutputStream;
  262. // This could fail, but try to add the stack trace as the body content
  263. try
  264. {
  265. stream.Write(bytes, 0, bytes.Length);
  266. }
  267. catch (Exception ex1)
  268. {
  269. //Logger.ErrorException("Error dumping stack trace", ex1);
  270. }
  271. }
  272. /// <summary>
  273. /// Processes the request internal.
  274. /// </summary>
  275. /// <param name="ctx">The CTX.</param>
  276. /// <returns>Task.</returns>
  277. private async Task ProcessRequestInternal(HttpListenerContext ctx)
  278. {
  279. var responseInfo = await GetResponseInfo().ConfigureAwait(false);
  280. // Let the client know if byte range requests are supported or not
  281. if (responseInfo.SupportsByteRangeRequests)
  282. {
  283. ctx.Response.Headers["Accept-Ranges"] = "bytes";
  284. }
  285. else if (!responseInfo.SupportsByteRangeRequests)
  286. {
  287. ctx.Response.Headers["Accept-Ranges"] = "none";
  288. }
  289. if (responseInfo.IsResponseValid && responseInfo.SupportsByteRangeRequests && IsRangeRequest)
  290. {
  291. // Set the initial status code
  292. // When serving a range request, we need to return status code 206 to indicate a partial response body
  293. responseInfo.StatusCode = 206;
  294. }
  295. ctx.Response.ContentType = responseInfo.ContentType;
  296. if (responseInfo.Etag.HasValue)
  297. {
  298. ctx.Response.Headers["ETag"] = responseInfo.Etag.Value.ToString("N");
  299. }
  300. var isCacheValid = true;
  301. // Validate If-Modified-Since
  302. if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
  303. {
  304. DateTime ifModifiedSince;
  305. if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
  306. {
  307. isCacheValid = IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration,
  308. responseInfo.DateLastModified);
  309. }
  310. }
  311. // Validate If-None-Match
  312. if (isCacheValid &&
  313. (responseInfo.Etag.HasValue || !string.IsNullOrEmpty(ctx.Request.Headers["If-None-Match"])))
  314. {
  315. Guid ifNoneMatch;
  316. if (Guid.TryParse(ctx.Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch))
  317. {
  318. if (responseInfo.Etag.HasValue && responseInfo.Etag.Value == ifNoneMatch)
  319. {
  320. responseInfo.StatusCode = 304;
  321. }
  322. }
  323. }
  324. LogResponse(ctx, responseInfo);
  325. if (responseInfo.IsResponseValid)
  326. {
  327. await OnProcessingRequest(responseInfo).ConfigureAwait(false);
  328. }
  329. if (responseInfo.IsResponseValid)
  330. {
  331. await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
  332. }
  333. else
  334. {
  335. if (responseInfo.StatusCode == 304)
  336. {
  337. AddAgeHeader(ctx.Response, responseInfo);
  338. AddExpiresHeader(ctx.Response, responseInfo);
  339. }
  340. ctx.Response.StatusCode = responseInfo.StatusCode;
  341. ctx.Response.SendChunked = false;
  342. }
  343. }
  344. /// <summary>
  345. /// The _null task result
  346. /// </summary>
  347. private readonly Task<bool> _nullTaskResult = Task.FromResult(true);
  348. /// <summary>
  349. /// Called when [processing request].
  350. /// </summary>
  351. /// <param name="responseInfo">The response info.</param>
  352. /// <returns>Task.</returns>
  353. protected virtual Task OnProcessingRequest(ResponseInfo responseInfo)
  354. {
  355. return _nullTaskResult;
  356. }
  357. /// <summary>
  358. /// Logs the response.
  359. /// </summary>
  360. /// <param name="ctx">The CTX.</param>
  361. /// <param name="responseInfo">The response info.</param>
  362. private void LogResponse(HttpListenerContext ctx, ResponseInfo responseInfo)
  363. {
  364. // Don't log normal 200's
  365. if (responseInfo.StatusCode == 200)
  366. {
  367. return;
  368. }
  369. var log = new StringBuilder();
  370. log.AppendLine(string.Format("Url: {0}", ctx.Request.Url));
  371. log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k])));
  372. var msg = "Http Response Sent (" + responseInfo.StatusCode + ") to " + ctx.Request.RemoteEndPoint;
  373. if (Kernel.Configuration.EnableHttpLevelLogging)
  374. {
  375. //Logger.LogMultiline(msg, LogSeverity.Debug, log);
  376. }
  377. }
  378. /// <summary>
  379. /// Processes the uncached request.
  380. /// </summary>
  381. /// <param name="ctx">The CTX.</param>
  382. /// <param name="responseInfo">The response info.</param>
  383. /// <returns>Task.</returns>
  384. private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
  385. {
  386. var totalContentLength = GetTotalContentLength(responseInfo);
  387. // By default, use chunked encoding if we don't know the content length
  388. var useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
  389. // Don't force this to true. HttpListener will default it to true if supported by the client.
  390. if (!useChunkedEncoding)
  391. {
  392. ctx.Response.SendChunked = false;
  393. }
  394. // Set the content length, if we know it
  395. if (totalContentLength.HasValue)
  396. {
  397. ctx.Response.ContentLength64 = totalContentLength.Value;
  398. }
  399. var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
  400. // Add the compression header
  401. if (compressResponse)
  402. {
  403. ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
  404. ctx.Response.AddHeader("Vary", "Accept-Encoding");
  405. }
  406. // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant
  407. // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching
  408. if (responseInfo.DateLastModified.HasValue && (!responseInfo.Etag.HasValue || responseInfo.CacheDuration.Ticks > 0))
  409. {
  410. ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
  411. AddAgeHeader(ctx.Response, responseInfo);
  412. }
  413. // Add caching headers
  414. ConfigureCaching(ctx.Response, responseInfo);
  415. // Set the status code
  416. ctx.Response.StatusCode = responseInfo.StatusCode;
  417. if (responseInfo.IsResponseValid)
  418. {
  419. // Finally, write the response data
  420. var outputStream = ctx.Response.OutputStream;
  421. if (compressResponse)
  422. {
  423. if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
  424. {
  425. CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, true);
  426. }
  427. else
  428. {
  429. CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, true);
  430. }
  431. outputStream = CompressedStream;
  432. }
  433. await WriteResponseToOutputStream(outputStream, responseInfo, totalContentLength).ConfigureAwait(false);
  434. }
  435. else
  436. {
  437. ctx.Response.SendChunked = false;
  438. }
  439. }
  440. /// <summary>
  441. /// Configures the caching.
  442. /// </summary>
  443. /// <param name="response">The response.</param>
  444. /// <param name="responseInfo">The response info.</param>
  445. private void ConfigureCaching(HttpListenerResponse response, ResponseInfo responseInfo)
  446. {
  447. if (responseInfo.CacheDuration.Ticks > 0)
  448. {
  449. response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(responseInfo.CacheDuration.TotalSeconds);
  450. }
  451. else if (responseInfo.Etag.HasValue)
  452. {
  453. response.Headers[HttpResponseHeader.CacheControl] = "public";
  454. }
  455. else
  456. {
  457. response.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, must-revalidate";
  458. response.Headers[HttpResponseHeader.Pragma] = "no-cache, no-store, must-revalidate";
  459. }
  460. AddExpiresHeader(response, responseInfo);
  461. }
  462. /// <summary>
  463. /// Adds the expires header.
  464. /// </summary>
  465. /// <param name="response">The response.</param>
  466. /// <param name="responseInfo">The response info.</param>
  467. private void AddExpiresHeader(HttpListenerResponse response, ResponseInfo responseInfo)
  468. {
  469. if (responseInfo.CacheDuration.Ticks > 0)
  470. {
  471. response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(responseInfo.CacheDuration).ToString("r");
  472. }
  473. else if (!responseInfo.Etag.HasValue)
  474. {
  475. response.Headers[HttpResponseHeader.Expires] = "-1";
  476. }
  477. }
  478. /// <summary>
  479. /// Adds the age header.
  480. /// </summary>
  481. /// <param name="response">The response.</param>
  482. /// <param name="responseInfo">The response info.</param>
  483. private void AddAgeHeader(HttpListenerResponse response, ResponseInfo responseInfo)
  484. {
  485. if (responseInfo.DateLastModified.HasValue)
  486. {
  487. response.Headers[HttpResponseHeader.Age] = Convert.ToInt32((DateTime.UtcNow - responseInfo.DateLastModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
  488. }
  489. }
  490. /// <summary>
  491. /// Writes the response to output stream.
  492. /// </summary>
  493. /// <param name="stream">The stream.</param>
  494. /// <param name="responseInfo">The response info.</param>
  495. /// <param name="contentLength">Length of the content.</param>
  496. /// <returns>Task.</returns>
  497. protected abstract Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength);
  498. /// <summary>
  499. /// Disposes the response stream.
  500. /// </summary>
  501. protected virtual void DisposeResponseStream()
  502. {
  503. if (CompressedStream != null)
  504. {
  505. try
  506. {
  507. CompressedStream.Dispose();
  508. }
  509. catch (Exception ex)
  510. {
  511. //Logger.ErrorException("Error disposing compressed stream", ex);
  512. }
  513. }
  514. try
  515. {
  516. //HttpListenerContext.Response.OutputStream.Dispose();
  517. HttpListenerContext.Response.Close();
  518. }
  519. catch (Exception ex)
  520. {
  521. //Logger.ErrorException("Error disposing response", ex);
  522. }
  523. }
  524. /// <summary>
  525. /// Determines whether [is cache valid] [the specified if modified since].
  526. /// </summary>
  527. /// <param name="ifModifiedSince">If modified since.</param>
  528. /// <param name="cacheDuration">Duration of the cache.</param>
  529. /// <param name="dateModified">The date modified.</param>
  530. /// <returns><c>true</c> if [is cache valid] [the specified if modified since]; otherwise, <c>false</c>.</returns>
  531. private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
  532. {
  533. if (dateModified.HasValue)
  534. {
  535. DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
  536. ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
  537. return lastModified <= ifModifiedSince;
  538. }
  539. DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
  540. if (DateTime.UtcNow < cacheExpirationDate)
  541. {
  542. return true;
  543. }
  544. return false;
  545. }
  546. /// <summary>
  547. /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
  548. /// </summary>
  549. /// <param name="date">The date.</param>
  550. /// <returns>DateTime.</returns>
  551. private DateTime NormalizeDateForComparison(DateTime date)
  552. {
  553. return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
  554. }
  555. /// <summary>
  556. /// Gets the total length of the content.
  557. /// </summary>
  558. /// <param name="responseInfo">The response info.</param>
  559. /// <returns>System.Nullable{System.Int64}.</returns>
  560. protected virtual long? GetTotalContentLength(ResponseInfo responseInfo)
  561. {
  562. return null;
  563. }
  564. /// <summary>
  565. /// Gets the response info.
  566. /// </summary>
  567. /// <returns>Task{ResponseInfo}.</returns>
  568. protected abstract Task<ResponseInfo> GetResponseInfo();
  569. /// <summary>
  570. /// Gets a bool query string param.
  571. /// </summary>
  572. /// <param name="name">The name.</param>
  573. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  574. protected bool GetBoolQueryStringParam(string name)
  575. {
  576. var val = QueryString[name] ?? string.Empty;
  577. return val.Equals("1", StringComparison.OrdinalIgnoreCase) || val.Equals("true", StringComparison.OrdinalIgnoreCase);
  578. }
  579. /// <summary>
  580. /// The _form values
  581. /// </summary>
  582. private Hashtable _formValues;
  583. /// <summary>
  584. /// Gets a value from form POST data
  585. /// </summary>
  586. /// <param name="name">The name.</param>
  587. /// <returns>Task{System.String}.</returns>
  588. protected async Task<string> GetFormValue(string name)
  589. {
  590. if (_formValues == null)
  591. {
  592. _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
  593. }
  594. if (_formValues.ContainsKey(name))
  595. {
  596. return _formValues[name].ToString();
  597. }
  598. return null;
  599. }
  600. /// <summary>
  601. /// Extracts form POST data from a request
  602. /// </summary>
  603. /// <param name="request">The request.</param>
  604. /// <returns>Task{Hashtable}.</returns>
  605. private async Task<Hashtable> GetFormValues(HttpListenerRequest request)
  606. {
  607. var formVars = new Hashtable();
  608. if (request.HasEntityBody)
  609. {
  610. if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
  611. {
  612. using (var requestBody = request.InputStream)
  613. {
  614. using (var reader = new StreamReader(requestBody, request.ContentEncoding))
  615. {
  616. var s = await reader.ReadToEndAsync().ConfigureAwait(false);
  617. var pairs = s.Split('&');
  618. foreach (var pair in pairs)
  619. {
  620. var index = pair.IndexOf('=');
  621. if (index != -1)
  622. {
  623. var name = pair.Substring(0, index);
  624. var value = pair.Substring(index + 1);
  625. formVars.Add(name, value);
  626. }
  627. }
  628. }
  629. }
  630. }
  631. }
  632. return formVars;
  633. }
  634. }
  635. /// <summary>
  636. /// Class ResponseInfo
  637. /// </summary>
  638. public class ResponseInfo
  639. {
  640. /// <summary>
  641. /// Gets or sets the type of the content.
  642. /// </summary>
  643. /// <value>The type of the content.</value>
  644. public string ContentType { get; set; }
  645. /// <summary>
  646. /// Gets or sets the etag.
  647. /// </summary>
  648. /// <value>The etag.</value>
  649. public Guid? Etag { get; set; }
  650. /// <summary>
  651. /// Gets or sets the date last modified.
  652. /// </summary>
  653. /// <value>The date last modified.</value>
  654. public DateTime? DateLastModified { get; set; }
  655. /// <summary>
  656. /// Gets or sets the duration of the cache.
  657. /// </summary>
  658. /// <value>The duration of the cache.</value>
  659. public TimeSpan CacheDuration { get; set; }
  660. /// <summary>
  661. /// Gets or sets a value indicating whether [compress response].
  662. /// </summary>
  663. /// <value><c>true</c> if [compress response]; otherwise, <c>false</c>.</value>
  664. public bool CompressResponse { get; set; }
  665. /// <summary>
  666. /// Gets or sets the status code.
  667. /// </summary>
  668. /// <value>The status code.</value>
  669. public int StatusCode { get; set; }
  670. /// <summary>
  671. /// Gets or sets a value indicating whether [supports byte range requests].
  672. /// </summary>
  673. /// <value><c>true</c> if [supports byte range requests]; otherwise, <c>false</c>.</value>
  674. public bool SupportsByteRangeRequests { get; set; }
  675. /// <summary>
  676. /// Initializes a new instance of the <see cref="ResponseInfo" /> class.
  677. /// </summary>
  678. public ResponseInfo()
  679. {
  680. CacheDuration = TimeSpan.FromTicks(0);
  681. CompressResponse = true;
  682. StatusCode = 200;
  683. }
  684. /// <summary>
  685. /// Gets a value indicating whether this instance is response valid.
  686. /// </summary>
  687. /// <value><c>true</c> if this instance is response valid; otherwise, <c>false</c>.</value>
  688. public bool IsResponseValid
  689. {
  690. get
  691. {
  692. return StatusCode >= 200 && StatusCode < 300;
  693. }
  694. }
  695. }
  696. }