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
{
}
}