using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpClientManager { /// /// Class HttpClientManager. /// public class HttpClientManager : IHttpClient { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IApplicationHost _appHost; /// /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. /// DON'T dispose it after use. /// /// The HTTP clients. private readonly ConcurrentDictionary _httpClients = new ConcurrentDictionary(); /// /// Initializes a new instance of the class. /// public HttpClientManager( IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IApplicationHost appHost) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem; _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); _appHost = appHost; } /// /// Gets the correct http client for the given url. /// /// The url. /// HttpClient. private HttpClient GetHttpClient(string url) { var key = GetHostFromUrl(url); if (!_httpClients.TryGetValue(key, out var client)) { client = new HttpClient() { BaseAddress = new Uri(url) }; _httpClients.TryAdd(key, client); } return client; } private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method) { string url = options.Url; var uriAddress = new Uri(url); string userInfo = uriAddress.UserInfo; if (!string.IsNullOrWhiteSpace(userInfo)) { _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal); } var request = new HttpRequestMessage(method, url); foreach (var header in options.RequestHeaders) { request.Headers.TryAddWithoutValidation(header.Key, header.Value); } if (options.EnableDefaultUserAgent && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _)) { request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent); } switch (options.DecompressionMethod) { case CompressionMethods.Deflate | CompressionMethods.Gzip: request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); break; case CompressionMethods.Deflate: request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); break; case CompressionMethods.Gzip: request.Headers.Add(HeaderNames.AcceptEncoding, "gzip"); break; default: break; } if (options.EnableKeepAlive) { request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); } // request.Headers.Add(HeaderNames.CacheControl, "no-cache"); /* if (!string.IsNullOrWhiteSpace(userInfo)) { var parts = userInfo.Split(':'); if (parts.Length == 2) { request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]); } } */ return request; } /// /// Gets the response internal. /// /// The options. /// Task{HttpResponseInfo}. public Task GetResponse(HttpRequestOptions options) => SendAsync(options, HttpMethod.Get); /// /// Performs a GET request and returns the resulting stream /// /// The options. /// Task{Stream}. public async Task Get(HttpRequestOptions options) { var response = await GetResponse(options).ConfigureAwait(false); return response.Content; } /// /// send as an asynchronous operation. /// /// The options. /// The HTTP method. /// Task{HttpResponseInfo}. public Task SendAsync(HttpRequestOptions options, string httpMethod) => SendAsync(options, new HttpMethod(httpMethod)); /// /// send as an asynchronous operation. /// /// The options. /// The HTTP method. /// Task{HttpResponseInfo}. public async Task SendAsync(HttpRequestOptions options, HttpMethod httpMethod) { if (options.CacheMode == CacheMode.None) { return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); } var url = options.Url; var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); var response = GetCachedResponse(responseCachePath, options.CacheLength, url); if (response != null) { return response; } response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.OK) { await CacheResponse(response, responseCachePath).ConfigureAwait(false); } return response; } private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) { if (File.Exists(responseCachePath) && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) { var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); return new HttpResponseInfo { ResponseUrl = url, Content = stream, StatusCode = HttpStatusCode.OK, ContentLength = stream.Length }; } return null; } private async Task CacheResponse(HttpResponseInfo response, string responseCachePath) { Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); using (var fileStream = new FileStream( responseCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true)) { await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); response.Content.Position = 0; } } private async Task SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod) { ValidateParams(options); options.CancellationToken.ThrowIfCancellationRequested(); var client = GetHttpClient(options.Url); var httpWebRequest = GetRequestMessage(options, httpMethod); if (!string.IsNullOrEmpty(options.RequestContent) || httpMethod == HttpMethod.Post) { if (options.RequestContent != null) { httpWebRequest.Content = new StringContent( options.RequestContent, null, options.RequestContentType); } else { httpWebRequest.Content = new ByteArrayContent(Array.Empty()); } } options.CancellationToken.ThrowIfCancellationRequested(); var response = await client.SendAsync( httpWebRequest, options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false); await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); options.CancellationToken.ThrowIfCancellationRequested(); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); return new HttpResponseInfo(response.Headers, response.Content.Headers) { Content = stream, StatusCode = response.StatusCode, ContentType = response.Content.Headers.ContentType?.MediaType, ContentLength = response.Content.Headers.ContentLength, ResponseUrl = response.Content.Headers.ContentLocation?.ToString() }; } /// public Task Post(HttpRequestOptions options) => SendAsync(options, HttpMethod.Post); private void ValidateParams(HttpRequestOptions options) { if (string.IsNullOrEmpty(options.Url)) { throw new ArgumentNullException(nameof(options)); } } /// /// Gets the host from URL. /// /// The URL. /// System.String. private static string GetHostFromUrl(string url) { var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase); if (index != -1) { url = url.Substring(index + 3); var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); if (!string.IsNullOrWhiteSpace(host)) { return host; } } return url; } private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) { if (response.IsSuccessStatusCode) { return; } if (options.LogErrorResponseBody) { string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); _logger.LogError("HTTP request failed with message: {Message}", msg); } throw new HttpException(response.ReasonPhrase) { StatusCode = response.StatusCode }; } } }