using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Octokit.Internal { /// /// Generic Http client. Useful for those who want to swap out System.Net.HttpClient with something else. /// /// /// Most folks won't ever need to swap this out. But if you're trying to run this on Windows Phone, you might. /// public class HttpClientAdapter : IHttpClient { readonly HttpClient _http; [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] public HttpClientAdapter(Func getHandler) { Ensure.ArgumentNotNull(getHandler, "getHandler"); _http = new HttpClient(new RedirectHandler { InnerHandler = getHandler() }); } /// /// Sends the specified request and returns a response. /// /// A that represents the HTTP request /// Used to cancel the request /// A of public async Task Send(IRequest request, CancellationToken cancellationToken) { Ensure.ArgumentNotNull(request, "request"); var cancellationTokenForRequest = GetCancellationTokenForRequest(request, cancellationToken); using (var requestMessage = BuildRequestMessage(request)) { // Make the request var responseMessage = await _http.SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationTokenForRequest) .ConfigureAwait(false); return await BuildResponse(responseMessage).ConfigureAwait(false); } } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] static CancellationToken GetCancellationTokenForRequest(IRequest request, CancellationToken cancellationToken) { var cancellationTokenForRequest = cancellationToken; if (request.Timeout != TimeSpan.Zero) { var timeoutCancellation = new CancellationTokenSource(request.Timeout); var unifiedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token); cancellationTokenForRequest = unifiedCancellationToken.Token; } return cancellationTokenForRequest; } protected async virtual Task BuildResponse(HttpResponseMessage responseMessage) { Ensure.ArgumentNotNull(responseMessage, "responseMessage"); object responseBody = null; string contentType = null; // We added support for downloading images,zip-files and application/octet-stream. // Let's constrain this appropriately. var binaryContentTypes = new[] { "application/zip" , "application/x-gzip" , "application/octet-stream"}; using (var content = responseMessage.Content) { if (content != null) { contentType = GetContentMediaType(responseMessage.Content); if (contentType != null && (contentType.StartsWith("image/") || binaryContentTypes .Any(item => item.Equals(contentType, StringComparison.OrdinalIgnoreCase)))) { responseBody = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } else { responseBody = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); } } } return new Response( responseMessage.StatusCode, responseBody, responseMessage.Headers.ToDictionary(h => h.Key, h => h.Value.First()), contentType); } protected virtual HttpRequestMessage BuildRequestMessage(IRequest request) { Ensure.ArgumentNotNull(request, "request"); HttpRequestMessage requestMessage = null; try { var fullUri = new Uri(request.BaseAddress, request.Endpoint); requestMessage = new HttpRequestMessage(request.Method, fullUri); foreach (var header in request.Headers) { requestMessage.Headers.Add(header.Key, header.Value); } var httpContent = request.Body as HttpContent; if (httpContent != null) { requestMessage.Content = httpContent; } var body = request.Body as string; if (body != null) { requestMessage.Content = new StringContent(body, Encoding.UTF8, request.ContentType); } var bodyStream = request.Body as Stream; if (bodyStream != null) { requestMessage.Content = new StreamContent(bodyStream); requestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(request.ContentType); } } catch (Exception) { if (requestMessage != null) { requestMessage.Dispose(); } throw; } return requestMessage; } static string GetContentMediaType(HttpContent httpContent) { if (httpContent.Headers != null && httpContent.Headers.ContentType != null) { return httpContent.Headers.ContentType.MediaType; } return null; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { if (_http != null) _http.Dispose(); } } } internal class RedirectHandler : DelegatingHandler { public const string RedirectCountKey = "RedirectCount"; public bool Enabled { get; set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); // Can't redirect without somewhere to redirect too. Throw? if (response.Headers.Location == null) return response; // Don't redirect if we exceed max number of redirects var redirectCount = 0; if (request.Properties.Keys.Contains(RedirectCountKey)) { redirectCount = (int)request.Properties[RedirectCountKey]; } if (redirectCount > 3) { throw new InvalidOperationException("The redirect count for this request has been exceeded. Aborting."); } request.Properties[RedirectCountKey] = ++redirectCount; if (response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.TemporaryRedirect || (int)response.StatusCode == 308) { var newRequest = CopyRequest(response.RequestMessage); if (response.StatusCode == HttpStatusCode.SeeOther) { newRequest.Content = null; newRequest.Method = HttpMethod.Get; } else { if (request.Content != null && request.Content.Headers.ContentLength != 0) { var stream = await request.Content.ReadAsStreamAsync(); if (stream.CanSeek) { stream.Position = 0; } else { throw new Exception("Cannot redirect a request with an unbuffered body"); } newRequest.Content = new StreamContent(stream); } } newRequest.RequestUri = response.Headers.Location; if (string.Compare(newRequest.RequestUri.Host, request.RequestUri.Host, StringComparison.OrdinalIgnoreCase) != 0) { newRequest.Headers.Authorization = null; } response = await SendAsync(newRequest, cancellationToken); } return response; } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] private static HttpRequestMessage CopyRequest(HttpRequestMessage oldRequest) { var newrequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri); foreach (var header in oldRequest.Headers) { newrequest.Headers.TryAddWithoutValidation(header.Key, header.Value); } foreach (var property in oldRequest.Properties) { newrequest.Properties.Add(property); } return newrequest; } } }