using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; 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; public const string RedirectCountKey = "RedirectCount"; [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")] public HttpClientAdapter(Func getHandler) { Ensure.ArgumentNotNull(getHandler, nameof(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 /// Function to preprocess HTTP response prior to deserialization (can be null) /// A of public async Task Send(IRequest request, CancellationToken cancellationToken, Func preprocessResponseBody = null) { Ensure.ArgumentNotNull(request, nameof(request)); var cancellationTokenForRequest = GetCancellationTokenForRequest(request, cancellationToken); using (var requestMessage = BuildRequestMessage(request)) { var responseMessage = await SendAsync(requestMessage, cancellationTokenForRequest).ConfigureAwait(false); return await BuildResponse(responseMessage, preprocessResponseBody).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 virtual async Task BuildResponse(HttpResponseMessage responseMessage, Func preprocessResponseBody) { Ensure.ArgumentNotNull(responseMessage, nameof(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[] { AcceptHeaders.RawContentMediaType, "application/zip" , "application/x-gzip" , "application/octet-stream"}; var content = responseMessage.Content; if (content != null) { contentType = GetContentMediaType(content); if (contentType != null && (contentType.StartsWith("image/") || binaryContentTypes .Any(item => item.Equals(contentType, StringComparison.OrdinalIgnoreCase)))) { responseBody = await content.ReadAsStreamAsync().ConfigureAwait(false); } else { responseBody = await content.ReadAsStringAsync().ConfigureAwait(false); content.Dispose(); } if (!(preprocessResponseBody is null)) responseBody = preprocessResponseBody(responseBody); } var responseHeaders = responseMessage.Headers.ToDictionary(h => h.Key, h => h.Value.First()); // Add Client response received time as a synthetic header const string receivedTimeHeaderName = ApiInfoParser.ReceivedTimeHeaderName; if (responseMessage.RequestMessage?.Properties is IDictionary reqProperties && reqProperties.TryGetValue(receivedTimeHeaderName, out object receivedTimeObj) && receivedTimeObj is string receivedTimeString && !responseHeaders.ContainsKey(receivedTimeHeaderName)) { responseHeaders[receivedTimeHeaderName] = receivedTimeString; } return new Response( responseMessage.StatusCode, responseBody, responseHeaders, contentType); } protected virtual HttpRequestMessage BuildRequestMessage(IRequest request) { Ensure.ArgumentNotNull(request, nameof(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?.ContentType != null) { return httpContent.Headers.ContentType.MediaType; } // Issue #2898 - Bad "zip" Content-Type coming from Blob Storage for artifacts if (httpContent.Headers?.TryGetValues("Content-Type", out var contentTypeValues) == true && contentTypeValues.FirstOrDefault() == "zip") { return "application/zip"; } return null; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { if (_http != null) _http.Dispose(); } } public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Clone the request/content in case we get a redirect var clonedRequest = await CloneHttpRequestMessageAsync(request).ConfigureAwait(false); // Send initial response var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); // Need to determine time on client computer as soon as possible. var receivedTime = DateTimeOffset.Now; // Since Properties are stored as objects, serialize to HTTP round-tripping string (Format: r) // Resolution is limited to one-second, matching the resolution of the HTTP Date header request.Properties[ApiInfoParser.ReceivedTimeHeaderName] = receivedTime.ToString("r", CultureInfo.InvariantCulture); // Can't redirect without somewhere to redirect to. 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."); } 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) { if (response.StatusCode == HttpStatusCode.SeeOther) { clonedRequest.Content = null; clonedRequest.Method = HttpMethod.Get; } // Increment the redirect count clonedRequest.Properties[RedirectCountKey] = ++redirectCount; // Set the new Uri based on location header clonedRequest.RequestUri = response.Headers.Location; // Clear authentication if redirected to a different host if (string.Compare(clonedRequest.RequestUri.Host, request.RequestUri.Host, StringComparison.OrdinalIgnoreCase) != 0) { clonedRequest.Headers.Authorization = null; } // Send redirected request response = await SendAsync(clonedRequest, cancellationToken).ConfigureAwait(false); } return response; } public static async Task CloneHttpRequestMessageAsync(HttpRequestMessage oldRequest) { var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri); // Copy the request's content (via a MemoryStream) into the cloned object var ms = new MemoryStream(); if (oldRequest.Content != null) { await oldRequest.Content.CopyToAsync(ms).ConfigureAwait(false); ms.Position = 0; newRequest.Content = new StreamContent(ms); // Copy the content headers if (oldRequest.Content.Headers != null) { foreach (var h in oldRequest.Content.Headers) { newRequest.Content.Headers.Add(h.Key, h.Value); } } } newRequest.Version = oldRequest.Version; 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; } /// /// Set the GitHub Api request timeout. /// /// The Timeout value public void SetRequestTimeout(TimeSpan timeout) { _http.Timeout = timeout; } } internal class RedirectHandler : DelegatingHandler { } }