HttpClientManager.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net;
  6. using System.Net.Http;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using MediaBrowser.Common.Configuration;
  10. using MediaBrowser.Common.Extensions;
  11. using MediaBrowser.Common.Net;
  12. using MediaBrowser.Model.IO;
  13. using MediaBrowser.Model.Net;
  14. using Microsoft.Extensions.Logging;
  15. using Microsoft.Net.Http.Headers;
  16. namespace Emby.Server.Implementations.HttpClientManager
  17. {
  18. /// <summary>
  19. /// Class HttpClientManager
  20. /// </summary>
  21. public class HttpClientManager : IHttpClient
  22. {
  23. /// <summary>
  24. /// The _logger
  25. /// </summary>
  26. private readonly ILogger _logger;
  27. /// <summary>
  28. /// The _app paths
  29. /// </summary>
  30. private readonly IApplicationPaths _appPaths;
  31. private readonly IFileSystem _fileSystem;
  32. private readonly Func<string> _defaultUserAgentFn;
  33. /// <summary>
  34. /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
  35. /// </summary>
  36. public HttpClientManager(
  37. IApplicationPaths appPaths,
  38. ILogger<HttpClientManager> logger,
  39. IFileSystem fileSystem,
  40. Func<string> defaultUserAgentFn)
  41. {
  42. if (appPaths == null)
  43. {
  44. throw new ArgumentNullException(nameof(appPaths));
  45. }
  46. if (logger == null)
  47. {
  48. throw new ArgumentNullException(nameof(logger));
  49. }
  50. _logger = logger;
  51. _fileSystem = fileSystem;
  52. _appPaths = appPaths;
  53. _defaultUserAgentFn = defaultUserAgentFn;
  54. }
  55. /// <summary>
  56. /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
  57. /// DON'T dispose it after use.
  58. /// </summary>
  59. /// <value>The HTTP clients.</value>
  60. private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
  61. /// <summary>
  62. /// Gets
  63. /// </summary>
  64. /// <param name="url">The host.</param>
  65. /// <param name="enableHttpCompression">if set to <c>true</c> [enable HTTP compression].</param>
  66. /// <returns>HttpClient.</returns>
  67. /// <exception cref="ArgumentNullException">host</exception>
  68. private HttpClient GetHttpClient(string url)
  69. {
  70. var key = GetHostFromUrl(url);
  71. if (!_httpClients.TryGetValue(key, out var client))
  72. {
  73. client = new HttpClient()
  74. {
  75. BaseAddress = new Uri(url)
  76. };
  77. _httpClients.TryAdd(key, client);
  78. }
  79. return client;
  80. }
  81. private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
  82. {
  83. string url = options.Url;
  84. var uriAddress = new Uri(url);
  85. string userInfo = uriAddress.UserInfo;
  86. if (!string.IsNullOrWhiteSpace(userInfo))
  87. {
  88. _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
  89. url = url.Replace(userInfo + '@', string.Empty);
  90. }
  91. var request = new HttpRequestMessage(method, url);
  92. AddRequestHeaders(request, options);
  93. switch (options.DecompressionMethod)
  94. {
  95. case CompressionMethod.Deflate | CompressionMethod.Gzip:
  96. request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
  97. break;
  98. case CompressionMethod.Deflate:
  99. request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
  100. break;
  101. case CompressionMethod.Gzip:
  102. request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
  103. break;
  104. case 0:
  105. default:
  106. break;
  107. }
  108. if (options.EnableKeepAlive)
  109. {
  110. request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
  111. }
  112. //request.Headers.Add(HeaderNames.CacheControl, "no-cache");
  113. /*
  114. if (!string.IsNullOrWhiteSpace(userInfo))
  115. {
  116. var parts = userInfo.Split(':');
  117. if (parts.Length == 2)
  118. {
  119. request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
  120. }
  121. }
  122. */
  123. return request;
  124. }
  125. private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
  126. {
  127. var hasUserAgent = false;
  128. foreach (var header in options.RequestHeaders)
  129. {
  130. if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
  131. {
  132. hasUserAgent = true;
  133. }
  134. request.Headers.Add(header.Key, header.Value);
  135. }
  136. if (!hasUserAgent && options.EnableDefaultUserAgent)
  137. {
  138. request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
  139. }
  140. }
  141. /// <summary>
  142. /// Gets the response internal.
  143. /// </summary>
  144. /// <param name="options">The options.</param>
  145. /// <returns>Task{HttpResponseInfo}.</returns>
  146. public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
  147. => SendAsync(options, HttpMethod.Get);
  148. /// <summary>
  149. /// Performs a GET request and returns the resulting stream
  150. /// </summary>
  151. /// <param name="options">The options.</param>
  152. /// <returns>Task{Stream}.</returns>
  153. public async Task<Stream> Get(HttpRequestOptions options)
  154. {
  155. var response = await GetResponse(options).ConfigureAwait(false);
  156. return response.Content;
  157. }
  158. /// <summary>
  159. /// send as an asynchronous operation.
  160. /// </summary>
  161. /// <param name="options">The options.</param>
  162. /// <param name="httpMethod">The HTTP method.</param>
  163. /// <returns>Task{HttpResponseInfo}.</returns>
  164. /// <exception cref="HttpException">
  165. /// </exception>
  166. public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
  167. {
  168. var httpMethod2 = GetHttpMethod(httpMethod);
  169. return SendAsync(options, httpMethod2);
  170. }
  171. /// <summary>
  172. /// send as an asynchronous operation.
  173. /// </summary>
  174. /// <param name="options">The options.</param>
  175. /// <param name="httpMethod">The HTTP method.</param>
  176. /// <returns>Task{HttpResponseInfo}.</returns>
  177. /// <exception cref="HttpException">
  178. /// </exception>
  179. public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
  180. {
  181. if (options.CacheMode == CacheMode.None)
  182. {
  183. return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
  184. }
  185. var url = options.Url;
  186. var urlHash = url.ToLowerInvariant().GetMD5().ToString("N");
  187. var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
  188. var response = GetCachedResponse(responseCachePath, options.CacheLength, url);
  189. if (response != null)
  190. {
  191. return response;
  192. }
  193. response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
  194. if (response.StatusCode == HttpStatusCode.OK)
  195. {
  196. await CacheResponse(response, responseCachePath).ConfigureAwait(false);
  197. }
  198. return response;
  199. }
  200. private HttpMethod GetHttpMethod(string httpMethod)
  201. {
  202. if (httpMethod.Equals("DELETE", StringComparison.OrdinalIgnoreCase))
  203. {
  204. return HttpMethod.Delete;
  205. }
  206. else if (httpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase))
  207. {
  208. return HttpMethod.Get;
  209. }
  210. else if (httpMethod.Equals("HEAD", StringComparison.OrdinalIgnoreCase))
  211. {
  212. return HttpMethod.Head;
  213. }
  214. else if (httpMethod.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase))
  215. {
  216. return HttpMethod.Options;
  217. }
  218. else if (httpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase))
  219. {
  220. return HttpMethod.Post;
  221. }
  222. else if (httpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase))
  223. {
  224. return HttpMethod.Put;
  225. }
  226. else if (httpMethod.Equals("TRACE", StringComparison.OrdinalIgnoreCase))
  227. {
  228. return HttpMethod.Trace;
  229. }
  230. throw new ArgumentException("Invalid HTTP method", nameof(httpMethod));
  231. }
  232. private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
  233. {
  234. if (File.Exists(responseCachePath)
  235. && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
  236. {
  237. var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true);
  238. return new HttpResponseInfo
  239. {
  240. ResponseUrl = url,
  241. Content = stream,
  242. StatusCode = HttpStatusCode.OK,
  243. ContentLength = stream.Length
  244. };
  245. }
  246. return null;
  247. }
  248. private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
  249. {
  250. Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
  251. using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true))
  252. {
  253. await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
  254. response.Content.Position = 0;
  255. }
  256. }
  257. private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
  258. {
  259. ValidateParams(options);
  260. options.CancellationToken.ThrowIfCancellationRequested();
  261. var client = GetHttpClient(options.Url);
  262. var httpWebRequest = GetRequestMessage(options, httpMethod);
  263. if (options.RequestContentBytes != null
  264. || !string.IsNullOrEmpty(options.RequestContent)
  265. || httpMethod == HttpMethod.Post)
  266. {
  267. if (options.RequestContentBytes != null)
  268. {
  269. httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes);
  270. }
  271. else if (options.RequestContent != null)
  272. {
  273. httpWebRequest.Content = new StringContent(options.RequestContent);
  274. }
  275. else
  276. {
  277. httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
  278. }
  279. /*
  280. var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
  281. if (options.AppendCharsetToMimeType)
  282. {
  283. contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\"";
  284. }
  285. httpWebRequest.Headers.Add(HeaderNames.ContentType, contentType);*/
  286. }
  287. if (options.LogRequest)
  288. {
  289. _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
  290. }
  291. options.CancellationToken.ThrowIfCancellationRequested();
  292. /*if (!options.BufferContent)
  293. {
  294. var response = await client.HttpClient.SendAsync(httpWebRequest).ConfigureAwait(false);
  295. await EnsureSuccessStatusCode(client, response, options).ConfigureAwait(false);
  296. options.CancellationToken.ThrowIfCancellationRequested();
  297. return GetResponseInfo(response, await response.Content.ReadAsStreamAsync().ConfigureAwait(false), response.Content.Headers.ContentLength, response);
  298. }*/
  299. using (var response = await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false))
  300. {
  301. await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
  302. options.CancellationToken.ThrowIfCancellationRequested();
  303. using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
  304. {
  305. var memoryStream = new MemoryStream();
  306. await stream.CopyToAsync(memoryStream, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
  307. memoryStream.Position = 0;
  308. var responseInfo = new HttpResponseInfo(response.Headers)
  309. {
  310. Content = memoryStream,
  311. StatusCode = response.StatusCode,
  312. ContentType = response.Content.Headers.ContentType?.MediaType,
  313. ContentLength = memoryStream.Length,
  314. ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
  315. };
  316. return responseInfo;
  317. }
  318. }
  319. }
  320. public Task<HttpResponseInfo> Post(HttpRequestOptions options)
  321. => SendAsync(options, HttpMethod.Post);
  322. /// <summary>
  323. /// Downloads the contents of a given url into a temporary location
  324. /// </summary>
  325. /// <param name="options">The options.</param>
  326. /// <returns>Task{System.String}.</returns>
  327. public async Task<string> GetTempFile(HttpRequestOptions options)
  328. {
  329. var response = await GetTempFileResponse(options).ConfigureAwait(false);
  330. return response.TempFilePath;
  331. }
  332. public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options)
  333. {
  334. ValidateParams(options);
  335. Directory.CreateDirectory(_appPaths.TempDirectory);
  336. var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
  337. if (options.Progress == null)
  338. {
  339. throw new ArgumentException("Options did not have a Progress value.", nameof(options));
  340. }
  341. options.CancellationToken.ThrowIfCancellationRequested();
  342. var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
  343. options.Progress.Report(0);
  344. if (options.LogRequest)
  345. {
  346. _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
  347. }
  348. var client = GetHttpClient(options.Url);
  349. try
  350. {
  351. options.CancellationToken.ThrowIfCancellationRequested();
  352. using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false)))
  353. {
  354. await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
  355. options.CancellationToken.ThrowIfCancellationRequested();
  356. using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
  357. using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
  358. {
  359. await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
  360. }
  361. options.Progress.Report(100);
  362. var responseInfo = new HttpResponseInfo(response.Headers)
  363. {
  364. TempFilePath = tempFile,
  365. StatusCode = response.StatusCode,
  366. ContentType = response.Content.Headers.ContentType?.MediaType,
  367. ContentLength = response.Content.Headers.ContentLength
  368. };
  369. return responseInfo;
  370. }
  371. }
  372. catch (Exception ex)
  373. {
  374. if (File.Exists(tempFile))
  375. {
  376. File.Delete(tempFile);
  377. }
  378. throw GetException(ex, options);
  379. }
  380. }
  381. private Exception GetException(Exception ex, HttpRequestOptions options)
  382. {
  383. if (ex is HttpException)
  384. {
  385. return ex;
  386. }
  387. var webException = ex as WebException
  388. ?? ex.InnerException as WebException;
  389. if (webException != null)
  390. {
  391. if (options.LogErrors)
  392. {
  393. _logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url);
  394. }
  395. var exception = new HttpException(webException.Message, webException);
  396. using (var response = webException.Response as HttpWebResponse)
  397. {
  398. if (response != null)
  399. {
  400. exception.StatusCode = response.StatusCode;
  401. }
  402. }
  403. if (!exception.StatusCode.HasValue)
  404. {
  405. if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
  406. webException.Status == WebExceptionStatus.ConnectFailure)
  407. {
  408. exception.IsTimedOut = true;
  409. }
  410. }
  411. return exception;
  412. }
  413. var operationCanceledException = ex as OperationCanceledException
  414. ?? ex.InnerException as OperationCanceledException;
  415. if (operationCanceledException != null)
  416. {
  417. return GetCancellationException(options, options.CancellationToken, operationCanceledException);
  418. }
  419. if (options.LogErrors)
  420. {
  421. _logger.LogError(ex, "Error getting response from {Url}", options.Url);
  422. }
  423. return ex;
  424. }
  425. private void ValidateParams(HttpRequestOptions options)
  426. {
  427. if (string.IsNullOrEmpty(options.Url))
  428. {
  429. throw new ArgumentNullException(nameof(options));
  430. }
  431. }
  432. /// <summary>
  433. /// Gets the host from URL.
  434. /// </summary>
  435. /// <param name="url">The URL.</param>
  436. /// <returns>System.String.</returns>
  437. private static string GetHostFromUrl(string url)
  438. {
  439. var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
  440. if (index != -1)
  441. {
  442. url = url.Substring(index + 3);
  443. var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
  444. if (!string.IsNullOrWhiteSpace(host))
  445. {
  446. return host;
  447. }
  448. }
  449. return url;
  450. }
  451. /// <summary>
  452. /// Throws the cancellation exception.
  453. /// </summary>
  454. /// <param name="options">The options.</param>
  455. /// <param name="cancellationToken">The cancellation token.</param>
  456. /// <param name="exception">The exception.</param>
  457. /// <returns>Exception.</returns>
  458. private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception)
  459. {
  460. // If the HttpClient's timeout is reached, it will cancel the Task internally
  461. if (!cancellationToken.IsCancellationRequested)
  462. {
  463. var msg = string.Format("Connection to {0} timed out", options.Url);
  464. if (options.LogErrors)
  465. {
  466. _logger.LogError(msg);
  467. }
  468. // Throw an HttpException so that the caller doesn't think it was cancelled by user code
  469. return new HttpException(msg, exception)
  470. {
  471. IsTimedOut = true
  472. };
  473. }
  474. return exception;
  475. }
  476. private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
  477. {
  478. if (response.IsSuccessStatusCode)
  479. {
  480. return;
  481. }
  482. var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
  483. _logger.LogError("HTTP request failed with message: {Message}", msg);
  484. throw new HttpException(response.ReasonPhrase)
  485. {
  486. StatusCode = response.StatusCode
  487. };
  488. }
  489. }
  490. }