2
0

HttpManager.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Common.Kernel;
  3. using MediaBrowser.Model.Net;
  4. using System;
  5. using System.Collections.Concurrent;
  6. using System.Collections.Generic;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Net;
  10. using System.Net.Cache;
  11. using System.Net.Http;
  12. using System.Text;
  13. using System.Threading;
  14. using System.Threading.Tasks;
  15. namespace MediaBrowser.Common.Net
  16. {
  17. /// <summary>
  18. /// Class HttpManager
  19. /// </summary>
  20. public class HttpManager : BaseManager<IKernel>
  21. {
  22. /// <summary>
  23. /// Initializes a new instance of the <see cref="HttpManager" /> class.
  24. /// </summary>
  25. /// <param name="kernel">The kernel.</param>
  26. public HttpManager(IKernel kernel)
  27. : base(kernel)
  28. {
  29. }
  30. /// <summary>
  31. /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
  32. /// DON'T dispose it after use.
  33. /// </summary>
  34. /// <value>The HTTP clients.</value>
  35. private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
  36. /// <summary>
  37. /// Gets
  38. /// </summary>
  39. /// <param name="host">The host.</param>
  40. /// <returns>HttpClient.</returns>
  41. /// <exception cref="System.ArgumentNullException">host</exception>
  42. private HttpClient GetHttpClient(string host)
  43. {
  44. if (string.IsNullOrEmpty(host))
  45. {
  46. throw new ArgumentNullException("host");
  47. }
  48. HttpClient client;
  49. if (!_httpClients.TryGetValue(host, out client))
  50. {
  51. var handler = new WebRequestHandler
  52. {
  53. AutomaticDecompression = DecompressionMethods.Deflate,
  54. CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate)
  55. };
  56. client = new HttpClient(handler);
  57. client.DefaultRequestHeaders.Add("Accept", "application/json,image/*");
  58. client.Timeout = TimeSpan.FromSeconds(15);
  59. _httpClients.TryAdd(host, client);
  60. }
  61. return client;
  62. }
  63. /// <summary>
  64. /// Performs a GET request and returns the resulting stream
  65. /// </summary>
  66. /// <param name="url">The URL.</param>
  67. /// <param name="resourcePool">The resource pool.</param>
  68. /// <param name="cancellationToken">The cancellation token.</param>
  69. /// <returns>Task{Stream}.</returns>
  70. /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
  71. public async Task<Stream> Get(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
  72. {
  73. ValidateParams(url, resourcePool, cancellationToken);
  74. cancellationToken.ThrowIfCancellationRequested();
  75. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  76. Logger.Info("HttpManager.Get url: {0}", url);
  77. try
  78. {
  79. cancellationToken.ThrowIfCancellationRequested();
  80. var msg = await GetHttpClient(GetHostFromUrl(url)).GetAsync(url, cancellationToken).ConfigureAwait(false);
  81. EnsureSuccessStatusCode(msg);
  82. return await msg.Content.ReadAsStreamAsync().ConfigureAwait(false);
  83. }
  84. catch (OperationCanceledException ex)
  85. {
  86. throw GetCancellationException(url, cancellationToken, ex);
  87. }
  88. catch (HttpRequestException ex)
  89. {
  90. Logger.ErrorException("Error getting response from " + url, ex);
  91. throw new HttpException(ex.Message, ex);
  92. }
  93. finally
  94. {
  95. resourcePool.Release();
  96. }
  97. }
  98. /// <summary>
  99. /// Performs a POST request
  100. /// </summary>
  101. /// <param name="url">The URL.</param>
  102. /// <param name="postData">Params to add to the POST data.</param>
  103. /// <param name="resourcePool">The resource pool.</param>
  104. /// <param name="cancellationToken">The cancellation token.</param>
  105. /// <returns>stream on success, null on failure</returns>
  106. /// <exception cref="System.ArgumentNullException">postData</exception>
  107. /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
  108. public async Task<Stream> Post(string url, Dictionary<string, string> postData, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
  109. {
  110. ValidateParams(url, resourcePool, cancellationToken);
  111. if (postData == null)
  112. {
  113. throw new ArgumentNullException("postData");
  114. }
  115. cancellationToken.ThrowIfCancellationRequested();
  116. var strings = postData.Keys.Select(key => string.Format("{0}={1}", key, postData[key]));
  117. var postContent = string.Join("&", strings.ToArray());
  118. var content = new StringContent(postContent, Encoding.UTF8, "application/x-www-form-urlencoded");
  119. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  120. Logger.Info("HttpManager.Post url: {0}", url);
  121. try
  122. {
  123. cancellationToken.ThrowIfCancellationRequested();
  124. var msg = await GetHttpClient(GetHostFromUrl(url)).PostAsync(url, content, cancellationToken).ConfigureAwait(false);
  125. EnsureSuccessStatusCode(msg);
  126. return await msg.Content.ReadAsStreamAsync().ConfigureAwait(false);
  127. }
  128. catch (OperationCanceledException ex)
  129. {
  130. throw GetCancellationException(url, cancellationToken, ex);
  131. }
  132. catch (HttpRequestException ex)
  133. {
  134. Logger.ErrorException("Error getting response from " + url, ex);
  135. throw new HttpException(ex.Message, ex);
  136. }
  137. finally
  138. {
  139. resourcePool.Release();
  140. }
  141. }
  142. /// <summary>
  143. /// Downloads the contents of a given url into a temporary location
  144. /// </summary>
  145. /// <param name="url">The URL.</param>
  146. /// <param name="resourcePool">The resource pool.</param>
  147. /// <param name="cancellationToken">The cancellation token.</param>
  148. /// <param name="progress">The progress.</param>
  149. /// <param name="userAgent">The user agent.</param>
  150. /// <returns>Task{System.String}.</returns>
  151. /// <exception cref="System.ArgumentNullException">progress</exception>
  152. /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
  153. public async Task<string> FetchToTempFile(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken, IProgress<double> progress, string userAgent = null)
  154. {
  155. ValidateParams(url, resourcePool, cancellationToken);
  156. if (progress == null)
  157. {
  158. throw new ArgumentNullException("progress");
  159. }
  160. cancellationToken.ThrowIfCancellationRequested();
  161. var tempFile = Path.Combine(Kernel.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".tmp");
  162. var message = new HttpRequestMessage(HttpMethod.Get, url);
  163. if (!string.IsNullOrEmpty(userAgent))
  164. {
  165. message.Headers.Add("User-Agent", userAgent);
  166. }
  167. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  168. Logger.Info("HttpManager.FetchToTempFile url: {0}, temp file: {1}", url, tempFile);
  169. try
  170. {
  171. cancellationToken.ThrowIfCancellationRequested();
  172. using (var response = await GetHttpClient(GetHostFromUrl(url)).SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
  173. {
  174. EnsureSuccessStatusCode(response);
  175. cancellationToken.ThrowIfCancellationRequested();
  176. IEnumerable<string> lengthValues;
  177. if (!response.Headers.TryGetValues("content-length", out lengthValues) &&
  178. !response.Content.Headers.TryGetValues("content-length", out lengthValues))
  179. {
  180. // We're not able to track progress
  181. using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
  182. {
  183. using (var fs = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
  184. {
  185. await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
  186. }
  187. }
  188. }
  189. else
  190. {
  191. var length = long.Parse(string.Join(string.Empty, lengthValues.ToArray()));
  192. using (var stream = ProgressStream.CreateReadProgressStream(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), progress.Report, length))
  193. {
  194. using (var fs = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
  195. {
  196. await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
  197. }
  198. }
  199. }
  200. progress.Report(100);
  201. cancellationToken.ThrowIfCancellationRequested();
  202. }
  203. return tempFile;
  204. }
  205. catch (OperationCanceledException ex)
  206. {
  207. // Cleanup
  208. if (File.Exists(tempFile))
  209. {
  210. File.Delete(tempFile);
  211. }
  212. throw GetCancellationException(url, cancellationToken, ex);
  213. }
  214. catch (HttpRequestException ex)
  215. {
  216. Logger.ErrorException("Error getting response from " + url, ex);
  217. // Cleanup
  218. if (File.Exists(tempFile))
  219. {
  220. File.Delete(tempFile);
  221. }
  222. throw new HttpException(ex.Message, ex);
  223. }
  224. catch (Exception ex)
  225. {
  226. Logger.ErrorException("Error getting response from " + url, ex);
  227. // Cleanup
  228. if (File.Exists(tempFile))
  229. {
  230. File.Delete(tempFile);
  231. }
  232. throw;
  233. }
  234. finally
  235. {
  236. resourcePool.Release();
  237. }
  238. }
  239. /// <summary>
  240. /// Downloads the contents of a given url into a MemoryStream
  241. /// </summary>
  242. /// <param name="url">The URL.</param>
  243. /// <param name="resourcePool">The resource pool.</param>
  244. /// <param name="cancellationToken">The cancellation token.</param>
  245. /// <returns>Task{MemoryStream}.</returns>
  246. /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
  247. public async Task<MemoryStream> FetchToMemoryStream(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
  248. {
  249. ValidateParams(url, resourcePool, cancellationToken);
  250. cancellationToken.ThrowIfCancellationRequested();
  251. var message = new HttpRequestMessage(HttpMethod.Get, url);
  252. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  253. var ms = new MemoryStream();
  254. Logger.Info("HttpManager.FetchToMemoryStream url: {0}", url);
  255. try
  256. {
  257. cancellationToken.ThrowIfCancellationRequested();
  258. using (var response = await GetHttpClient(GetHostFromUrl(url)).SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
  259. {
  260. EnsureSuccessStatusCode(response);
  261. cancellationToken.ThrowIfCancellationRequested();
  262. using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
  263. {
  264. await stream.CopyToAsync(ms, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
  265. }
  266. cancellationToken.ThrowIfCancellationRequested();
  267. }
  268. ms.Position = 0;
  269. return ms;
  270. }
  271. catch (OperationCanceledException ex)
  272. {
  273. ms.Dispose();
  274. throw GetCancellationException(url, cancellationToken, ex);
  275. }
  276. catch (HttpRequestException ex)
  277. {
  278. Logger.ErrorException("Error getting response from " + url, ex);
  279. ms.Dispose();
  280. throw new HttpException(ex.Message, ex);
  281. }
  282. catch (Exception ex)
  283. {
  284. Logger.ErrorException("Error getting response from " + url, ex);
  285. ms.Dispose();
  286. throw;
  287. }
  288. finally
  289. {
  290. resourcePool.Release();
  291. }
  292. }
  293. /// <summary>
  294. /// Validates the params.
  295. /// </summary>
  296. /// <param name="url">The URL.</param>
  297. /// <param name="resourcePool">The resource pool.</param>
  298. /// <param name="cancellationToken">The cancellation token.</param>
  299. /// <exception cref="System.ArgumentNullException">url</exception>
  300. private void ValidateParams(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
  301. {
  302. if (string.IsNullOrEmpty(url))
  303. {
  304. throw new ArgumentNullException("url");
  305. }
  306. if (resourcePool == null)
  307. {
  308. throw new ArgumentNullException("resourcePool");
  309. }
  310. if (cancellationToken == null)
  311. {
  312. throw new ArgumentNullException("cancellationToken");
  313. }
  314. }
  315. /// <summary>
  316. /// Gets the host from URL.
  317. /// </summary>
  318. /// <param name="url">The URL.</param>
  319. /// <returns>System.String.</returns>
  320. private string GetHostFromUrl(string url)
  321. {
  322. var start = url.IndexOf("://", StringComparison.OrdinalIgnoreCase) + 3;
  323. var len = url.IndexOf('/', start) - start;
  324. return url.Substring(start, len);
  325. }
  326. /// <summary>
  327. /// Releases unmanaged and - optionally - managed resources.
  328. /// </summary>
  329. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  330. protected override void Dispose(bool dispose)
  331. {
  332. if (dispose)
  333. {
  334. foreach (var client in _httpClients.Values.ToList())
  335. {
  336. client.Dispose();
  337. }
  338. _httpClients.Clear();
  339. }
  340. base.Dispose(dispose);
  341. }
  342. /// <summary>
  343. /// Throws the cancellation exception.
  344. /// </summary>
  345. /// <param name="url">The URL.</param>
  346. /// <param name="cancellationToken">The cancellation token.</param>
  347. /// <param name="exception">The exception.</param>
  348. /// <returns>Exception.</returns>
  349. private Exception GetCancellationException(string url, CancellationToken cancellationToken, OperationCanceledException exception)
  350. {
  351. // If the HttpClient's timeout is reached, it will cancel the Task internally
  352. if (!cancellationToken.IsCancellationRequested)
  353. {
  354. var msg = string.Format("Connection to {0} timed out", url);
  355. Logger.Error(msg);
  356. // Throw an HttpException so that the caller doesn't think it was cancelled by user code
  357. return new HttpException(msg, exception) { IsTimedOut = true };
  358. }
  359. return exception;
  360. }
  361. /// <summary>
  362. /// Ensures the success status code.
  363. /// </summary>
  364. /// <param name="response">The response.</param>
  365. /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
  366. private void EnsureSuccessStatusCode(HttpResponseMessage response)
  367. {
  368. if (!response.IsSuccessStatusCode)
  369. {
  370. throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode };
  371. }
  372. }
  373. }
  374. }