mirror of
https://github.com/zoriya/octokit.net.git
synced 2026-06-01 02:18:44 +00:00
328 lines
13 KiB
C#
328 lines
13 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// Generic Http client. Useful for those who want to swap out System.Net.HttpClient with something else.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Most folks won't ever need to swap this out. But if you're trying to run this on Windows Phone, you might.
|
||
/// </remarks>
|
||
public class HttpClientAdapter : IHttpClient
|
||
{
|
||
readonly HttpClient _http;
|
||
|
||
public const string RedirectCountKey = "RedirectCount";
|
||
|
||
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
||
public HttpClientAdapter(Func<HttpMessageHandler> getHandler)
|
||
{
|
||
Ensure.ArgumentNotNull(getHandler, nameof(getHandler));
|
||
|
||
#if HAS_SERVICEPOINTMANAGER
|
||
// GitHub API requires TLS1.2 as of February 2018
|
||
//
|
||
// .NET Framework before 4.6 did not enable TLS1.2 by default
|
||
//
|
||
// Even though this is an AppDomain wide setting, the decision was made for Octokit to
|
||
// ensure that TLS1.2 is enabled so that existing applications using Octokit did not need to
|
||
// make changes outside Octokit to continue to work with GitHub API
|
||
//
|
||
// *Update*
|
||
// .NET Framework 4.7 introduced a new value (SecurityProtocolType.SystemDefault = 0)
|
||
// which defers enabled protocols to operating system defaults
|
||
// If this is the current value we shouldn't do anything, as that would cause TLS1.2 to be the ONLY enabled protocol!
|
||
//
|
||
// See https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=netframework-4.7
|
||
// See https://github.com/octokit/octokit.net/issues/1914
|
||
|
||
// Only apply when current setting is not SystemDefault (0) added in .NET 4.7
|
||
if ((int)ServicePointManager.SecurityProtocol != 0)
|
||
{
|
||
// Add Tls1.2 to the existing enabled protocols
|
||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||
}
|
||
#endif
|
||
|
||
_http = new HttpClient(new RedirectHandler { InnerHandler = getHandler() });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sends the specified request and returns a response.
|
||
/// </summary>
|
||
/// <param name="request">A <see cref="IRequest"/> that represents the HTTP request</param>
|
||
/// <param name="cancellationToken">Used to cancel the request</param>
|
||
/// <returns>A <see cref="Task" /> of <see cref="IResponse"/></returns>
|
||
public async Task<IResponse> Send(IRequest request, CancellationToken cancellationToken)
|
||
{
|
||
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).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<IResponse> BuildResponse(HttpResponseMessage responseMessage)
|
||
{
|
||
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"};
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
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<string, object> 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 != 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();
|
||
}
|
||
}
|
||
|
||
public async Task<HttpResponseMessage> 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<HttpRequestMessage> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Set the GitHub Api request timeout.
|
||
/// </summary>
|
||
/// <param name="timeout">The Timeout value</param>
|
||
public void SetRequestTimeout(TimeSpan timeout)
|
||
{
|
||
_http.Timeout = timeout;
|
||
}
|
||
}
|
||
|
||
internal class RedirectHandler : DelegatingHandler
|
||
{
|
||
}
|
||
}
|