mirror of
https://github.com/zoriya/octokit.net.git
synced 2026-05-12 19:05:24 +00:00
637 lines
27 KiB
C#
637 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Octokit.Internal;
|
|
|
|
namespace Octokit
|
|
{
|
|
// NOTE: Every request method must go through the `RunRequest` code path. So if you need to add a new method
|
|
// ensure it goes through there. :)
|
|
/// <summary>
|
|
/// A connection for making HTTP requests against URI endpoints.
|
|
/// </summary>
|
|
public class Connection : IConnection
|
|
{
|
|
static readonly Uri _defaultGitHubApiUrl = GitHubClient.GitHubApiUrl;
|
|
static readonly ICredentialStore _anonymousCredentials = new InMemoryCredentialStore(Credentials.Anonymous);
|
|
|
|
readonly Authenticator _authenticator;
|
|
readonly JsonHttpPipeline _jsonPipeline;
|
|
readonly IHttpClient _httpClient;
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
public Connection(ProductHeaderValue productInformation)
|
|
: this(productInformation, _defaultGitHubApiUrl, _anonymousCredentials)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
/// <param name="httpClient">
|
|
/// The client to use for executing requests
|
|
/// </param>
|
|
public Connection(ProductHeaderValue productInformation, IHttpClient httpClient)
|
|
: this(productInformation, _defaultGitHubApiUrl, _anonymousCredentials, httpClient, new SimpleJsonSerializer())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
/// <param name="baseAddress">
|
|
/// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise
|
|
/// instance</param>
|
|
public Connection(ProductHeaderValue productInformation, Uri baseAddress)
|
|
: this(productInformation, baseAddress, _anonymousCredentials)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
/// <param name="credentialStore">Provides credentials to the client when making requests</param>
|
|
public Connection(ProductHeaderValue productInformation, ICredentialStore credentialStore)
|
|
: this(productInformation, _defaultGitHubApiUrl, credentialStore)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
/// <param name="baseAddress">
|
|
/// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise
|
|
/// instance</param>
|
|
/// <param name="credentialStore">Provides credentials to the client when making requests</param>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
|
|
public Connection(ProductHeaderValue productInformation, Uri baseAddress, ICredentialStore credentialStore)
|
|
: this(productInformation, baseAddress, credentialStore, new HttpClientAdapter(HttpMessageHandlerFactory.CreateDefault), new SimpleJsonSerializer())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new connection instance used to make requests of the GitHub API.
|
|
/// </summary>
|
|
/// <param name="productInformation">
|
|
/// The name (and optionally version) of the product using this library. This is sent to the server as part of
|
|
/// the user agent for analytics purposes.
|
|
/// </param>
|
|
/// <param name="baseAddress">
|
|
/// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise
|
|
/// instance</param>
|
|
/// <param name="credentialStore">Provides credentials to the client when making requests</param>
|
|
/// <param name="httpClient">A raw <see cref="IHttpClient"/> used to make requests</param>
|
|
/// <param name="serializer">Class used to serialize and deserialize JSON requests</param>
|
|
public Connection(
|
|
ProductHeaderValue productInformation,
|
|
Uri baseAddress,
|
|
ICredentialStore credentialStore,
|
|
IHttpClient httpClient,
|
|
IJsonSerializer serializer)
|
|
{
|
|
Ensure.ArgumentNotNull(productInformation, "productInformation");
|
|
Ensure.ArgumentNotNull(baseAddress, "baseAddress");
|
|
Ensure.ArgumentNotNull(credentialStore, "credentialStore");
|
|
Ensure.ArgumentNotNull(httpClient, "httpClient");
|
|
Ensure.ArgumentNotNull(serializer, "serializer");
|
|
|
|
if (!baseAddress.IsAbsoluteUri)
|
|
{
|
|
throw new ArgumentException(
|
|
string.Format(CultureInfo.InvariantCulture, "The base address '{0}' must be an absolute URI",
|
|
baseAddress), "baseAddress");
|
|
}
|
|
|
|
UserAgent = FormatUserAgent(productInformation);
|
|
BaseAddress = baseAddress;
|
|
_authenticator = new Authenticator(credentialStore);
|
|
_httpClient = httpClient;
|
|
_jsonPipeline = new JsonHttpPipeline(serializer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the latest API Info - this will be null if no API calls have been made
|
|
/// </summary>
|
|
/// <returns><seealso cref="ApiInfo"/> representing the information returned as part of an Api call</returns>
|
|
public ApiInfo GetLastApiInfo()
|
|
{
|
|
// We've chosen to not wrap the _lastApiInfo in a lock. Originally the code was returning a reference - so there was a danger of
|
|
// on thread writing to the object while another was reading. Now we are cloning the ApiInfo on request - thus removing the need (or overhead)
|
|
// of putting locks in place.
|
|
// See https://github.com/octokit/octokit.net/pull/855#discussion_r36774884
|
|
return _lastApiInfo == null ? null : _lastApiInfo.Clone();
|
|
}
|
|
private ApiInfo _lastApiInfo;
|
|
|
|
public Task<IApiResponse<T>> Get<T>(Uri uri, IDictionary<string, string> parameters, string accepts)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
return SendData<T>(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, CancellationToken.None);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Get<T>(Uri uri, IDictionary<string, string> parameters, string accepts, CancellationToken cancellationToken)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
return SendData<T>(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, cancellationToken);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Get<T>(Uri uri, TimeSpan timeout)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
return SendData<T>(uri, HttpMethod.Get, null, null, null, timeout, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP GET request that expects a <seealso cref="IResponse"/> containing HTML.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <param name="parameters">Querystring parameters for the request</param>
|
|
/// <returns><seealso cref="IResponse"/> representing the received HTTP response</returns>
|
|
public Task<IApiResponse<string>> GetHtml(Uri uri, IDictionary<string, string> parameters)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
return GetHtml(new Request
|
|
{
|
|
Method = HttpMethod.Get,
|
|
BaseAddress = BaseAddress,
|
|
Endpoint = uri.ApplyParameters(parameters)
|
|
});
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Patch<T>(Uri uri, object body)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
|
|
return SendData<T>(uri, HttpVerb.Patch, body, null, null, CancellationToken.None);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Patch<T>(Uri uri, object body, string accepts)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
Ensure.ArgumentNotNull(accepts, "accepts");
|
|
|
|
return SendData<T>(uri, HttpVerb.Patch, body, accepts, null, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP POST request.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <returns><seealso cref="IResponse"/> representing the received HTTP response</returns>
|
|
public async Task<HttpStatusCode> Post(Uri uri)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var response = await SendData<object>(uri, HttpMethod.Post, null, null, null, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Post<T>(Uri uri)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
return SendData<T>(uri, HttpMethod.Post, null, null, null, CancellationToken.None);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
|
|
return SendData<T>(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP POST request.
|
|
/// Attempts to map the response body to an object of type <typeparamref name="T"/>
|
|
/// </summary>
|
|
/// <typeparam name="T">The type to map the response to</typeparam>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <param name="body">The object to serialize as the body of the request</param>
|
|
/// <param name="accepts">Specifies accepted response media types.</param>
|
|
/// <param name="contentType">Specifies the media type of the request body</param>
|
|
/// <param name="twoFactorAuthenticationCode">Two Factor Authentication Code</param>
|
|
/// <returns><seealso cref="IResponse"/> representing the received HTTP response</returns>
|
|
public Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType, string twoFactorAuthenticationCode)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode");
|
|
|
|
return SendData<T>(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, twoFactorAuthenticationCode);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType, TimeSpan timeout)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
|
|
return SendData<T>(uri, HttpMethod.Post, body, accepts, contentType, timeout, CancellationToken.None);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType, Uri baseAddress)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(body, "body");
|
|
|
|
return SendData<T>(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, baseAddress: baseAddress);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Put<T>(Uri uri, object body)
|
|
{
|
|
return SendData<T>(uri, HttpMethod.Put, body, null, null, CancellationToken.None);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Put<T>(Uri uri, object body, string twoFactorAuthenticationCode)
|
|
{
|
|
return SendData<T>(uri,
|
|
HttpMethod.Put,
|
|
body,
|
|
null,
|
|
null,
|
|
CancellationToken.None,
|
|
twoFactorAuthenticationCode);
|
|
}
|
|
|
|
public Task<IApiResponse<T>> Put<T>(Uri uri, object body, string twoFactorAuthenticationCode, string accepts)
|
|
{
|
|
return SendData<T>(uri,
|
|
HttpMethod.Put,
|
|
body,
|
|
accepts,
|
|
null,
|
|
CancellationToken.None,
|
|
twoFactorAuthenticationCode);
|
|
}
|
|
|
|
Task<IApiResponse<T>> SendData<T>(
|
|
Uri uri,
|
|
HttpMethod method,
|
|
object body,
|
|
string accepts,
|
|
string contentType,
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken,
|
|
string twoFactorAuthenticationCode = null,
|
|
Uri baseAddress = null)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.GreaterThanZero(timeout, "timeout");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = method,
|
|
BaseAddress = baseAddress ?? BaseAddress,
|
|
Endpoint = uri,
|
|
Timeout = timeout
|
|
};
|
|
|
|
return SendDataInternal<T>(body, accepts, contentType, cancellationToken, twoFactorAuthenticationCode, request);
|
|
}
|
|
|
|
Task<IApiResponse<T>> SendData<T>(
|
|
Uri uri,
|
|
HttpMethod method,
|
|
object body,
|
|
string accepts,
|
|
string contentType,
|
|
CancellationToken cancellationToken,
|
|
string twoFactorAuthenticationCode = null,
|
|
Uri baseAddress = null)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = method,
|
|
BaseAddress = baseAddress ?? BaseAddress,
|
|
Endpoint = uri
|
|
};
|
|
|
|
return SendDataInternal<T>(body, accepts, contentType, cancellationToken, twoFactorAuthenticationCode, request);
|
|
}
|
|
|
|
Task<IApiResponse<T>> SendDataInternal<T>(object body, string accepts, string contentType, CancellationToken cancellationToken, string twoFactorAuthenticationCode, Request request)
|
|
{
|
|
if (!string.IsNullOrEmpty(accepts))
|
|
{
|
|
request.Headers["Accept"] = accepts;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(twoFactorAuthenticationCode))
|
|
{
|
|
request.Headers["X-GitHub-OTP"] = twoFactorAuthenticationCode;
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
request.Body = body;
|
|
// Default Content Type per: http://developer.github.com/v3/
|
|
request.ContentType = contentType ?? "application/x-www-form-urlencoded";
|
|
}
|
|
|
|
return Run<T>(request, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP PATCH request.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <returns><seealso cref="IResponse"/> representing the received HTTP response</returns>
|
|
public async Task<HttpStatusCode> Patch(Uri uri)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = HttpVerb.Patch,
|
|
BaseAddress = BaseAddress,
|
|
Endpoint = uri
|
|
};
|
|
var response = await Run<object>(request, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP PUT request that expects an empty response.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
|
|
public async Task<HttpStatusCode> Put(Uri uri)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = HttpMethod.Put,
|
|
BaseAddress = BaseAddress,
|
|
Endpoint = uri
|
|
};
|
|
var response = await Run<object>(request, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
|
|
public async Task<HttpStatusCode> Delete(Uri uri)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = HttpMethod.Delete,
|
|
BaseAddress = BaseAddress,
|
|
Endpoint = uri
|
|
};
|
|
var response = await Run<object>(request, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <param name="twoFactorAuthenticationCode">Two Factor Code</param>
|
|
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
|
|
public async Task<HttpStatusCode> Delete(Uri uri, string twoFactorAuthenticationCode)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
|
|
var response = await SendData<object>(uri, HttpMethod.Delete, null, null, null, CancellationToken.None, twoFactorAuthenticationCode).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <param name="data">The object to serialize as the body of the request</param>
|
|
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
|
|
public async Task<HttpStatusCode> Delete(Uri uri, object data)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(data, "data");
|
|
|
|
var request = new Request
|
|
{
|
|
Method = HttpMethod.Delete,
|
|
Body = data,
|
|
BaseAddress = BaseAddress,
|
|
Endpoint = uri
|
|
};
|
|
var response = await Run<object>(request, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an asynchronous HTTP DELETE request that expects an empty response.
|
|
/// </summary>
|
|
/// <param name="uri">URI endpoint to send request to</param>
|
|
/// <param name="data">The object to serialize as the body of the request</param>
|
|
/// <param name="accepts">Specifies accept response media type</param>
|
|
/// <returns>The returned <seealso cref="HttpStatusCode"/></returns>
|
|
public async Task<HttpStatusCode> Delete(Uri uri, object data, string accepts)
|
|
{
|
|
Ensure.ArgumentNotNull(uri, "uri");
|
|
Ensure.ArgumentNotNull(accepts, "accepts");
|
|
|
|
var response = await SendData<object>(uri, HttpMethod.Delete, data, accepts, null, CancellationToken.None).ConfigureAwait(false);
|
|
return response.HttpResponse.StatusCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base address for the connection.
|
|
/// </summary>
|
|
public Uri BaseAddress { get; private set; }
|
|
|
|
public string UserAgent { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the <seealso cref="ICredentialStore"/> used to provide credentials for the connection.
|
|
/// </summary>
|
|
public ICredentialStore CredentialStore
|
|
{
|
|
get { return _authenticator.CredentialStore; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the credentials used by the connection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// You can use this property if you only have a single hard-coded credential. Otherwise, pass in an
|
|
/// <see cref="ICredentialStore"/> to the constructor.
|
|
/// Setting this property will change the <see cref="ICredentialStore"/> to use
|
|
/// the default <see cref="InMemoryCredentialStore"/> with just these credentials.
|
|
/// </remarks>
|
|
public Credentials Credentials
|
|
{
|
|
get
|
|
{
|
|
var credentialTask = CredentialStore.GetCredentials();
|
|
if (credentialTask == null) return Credentials.Anonymous;
|
|
return credentialTask.Result ?? Credentials.Anonymous;
|
|
}
|
|
// Note this is for convenience. We probably shouldn't allow this to be mutable.
|
|
set
|
|
{
|
|
Ensure.ArgumentNotNull(value, "value");
|
|
_authenticator.CredentialStore = new InMemoryCredentialStore(value);
|
|
}
|
|
}
|
|
|
|
async Task<IApiResponse<string>> GetHtml(IRequest request)
|
|
{
|
|
request.Headers.Add("Accept", AcceptHeaders.StableVersionHtml);
|
|
var response = await RunRequest(request, CancellationToken.None).ConfigureAwait(false);
|
|
return new ApiResponse<string>(response, response.Body as string);
|
|
}
|
|
|
|
async Task<IApiResponse<T>> Run<T>(IRequest request, CancellationToken cancellationToken)
|
|
{
|
|
_jsonPipeline.SerializeRequest(request);
|
|
var response = await RunRequest(request, cancellationToken).ConfigureAwait(false);
|
|
return _jsonPipeline.DeserializeResponse<T>(response);
|
|
}
|
|
|
|
// THIS IS THE METHOD THAT EVERY REQUEST MUST GO THROUGH!
|
|
async Task<IResponse> RunRequest(IRequest request, CancellationToken cancellationToken)
|
|
{
|
|
request.Headers.Add("User-Agent", UserAgent);
|
|
await _authenticator.Apply(request).ConfigureAwait(false);
|
|
var response = await _httpClient.Send(request, cancellationToken).ConfigureAwait(false);
|
|
if (response != null)
|
|
{
|
|
// Use the clone method to avoid keeping hold of the original (just in case it effect the lifetime of the whole response
|
|
_lastApiInfo = response.ApiInfo.Clone();
|
|
}
|
|
HandleErrors(response);
|
|
return response;
|
|
}
|
|
|
|
static readonly Dictionary<HttpStatusCode, Func<IResponse, Exception>> _httpExceptionMap =
|
|
new Dictionary<HttpStatusCode, Func<IResponse, Exception>>
|
|
{
|
|
{ HttpStatusCode.Unauthorized, GetExceptionForUnauthorized },
|
|
{ HttpStatusCode.Forbidden, GetExceptionForForbidden },
|
|
{ HttpStatusCode.NotFound, response => new NotFoundException(response) },
|
|
{ (HttpStatusCode)422, response => new ApiValidationException(response) },
|
|
{ (HttpStatusCode)451, response => new LegalRestrictionException(response) }
|
|
};
|
|
|
|
static void HandleErrors(IResponse response)
|
|
{
|
|
Func<IResponse, Exception> exceptionFunc;
|
|
if (_httpExceptionMap.TryGetValue(response.StatusCode, out exceptionFunc))
|
|
{
|
|
throw exceptionFunc(response);
|
|
}
|
|
|
|
if ((int)response.StatusCode >= 400)
|
|
{
|
|
throw new ApiException(response);
|
|
}
|
|
}
|
|
|
|
static Exception GetExceptionForUnauthorized(IResponse response)
|
|
{
|
|
var twoFactorType = ParseTwoFactorType(response);
|
|
|
|
return twoFactorType == TwoFactorType.None
|
|
? new AuthorizationException(response)
|
|
: new TwoFactorRequiredException(response, twoFactorType);
|
|
}
|
|
|
|
static Exception GetExceptionForForbidden(IResponse response)
|
|
{
|
|
string body = response.Body as string ?? "";
|
|
return body.Contains("rate limit exceeded")
|
|
? new RateLimitExceededException(response)
|
|
: body.Contains("number of login attempts exceeded")
|
|
? new LoginAttemptsExceededException(response)
|
|
: new ForbiddenException(response);
|
|
}
|
|
|
|
internal static TwoFactorType ParseTwoFactorType(IResponse restResponse)
|
|
{
|
|
if (restResponse == null || restResponse.Headers == null || !restResponse.Headers.Any()) return TwoFactorType.None;
|
|
var otpHeader = restResponse.Headers.FirstOrDefault(header =>
|
|
header.Key.Equals("X-GitHub-OTP", StringComparison.OrdinalIgnoreCase));
|
|
if (string.IsNullOrEmpty(otpHeader.Value)) return TwoFactorType.None;
|
|
var factorType = otpHeader.Value;
|
|
var parts = factorType.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length > 0 && parts[0] == "required")
|
|
{
|
|
var secondPart = parts.Length > 1 ? parts[1].Trim() : null;
|
|
switch (secondPart)
|
|
{
|
|
case "sms":
|
|
return TwoFactorType.Sms;
|
|
case "app":
|
|
return TwoFactorType.AuthenticatorApp;
|
|
default:
|
|
return TwoFactorType.Unknown;
|
|
}
|
|
}
|
|
return TwoFactorType.None;
|
|
}
|
|
|
|
static string FormatUserAgent(ProductHeaderValue productInformation)
|
|
{
|
|
return string.Format(CultureInfo.InvariantCulture,
|
|
"{0} ({1} {2}; {3}; {4}; Octokit {5})",
|
|
productInformation,
|
|
#if NETFX_CORE
|
|
// Microsoft doesn't want you changing your Windows Store Application based on the processor or
|
|
// Windows version. If we really wanted this information, we could do a best guess based on
|
|
// this approach: http://attackpattern.com/2013/03/device-information-in-windows-8-store-apps/
|
|
// But I don't think we care all that much.
|
|
"WindowsRT",
|
|
"8+",
|
|
"unknown",
|
|
#else
|
|
Environment.OSVersion.Platform,
|
|
Environment.OSVersion.Version.ToString(3),
|
|
Environment.Is64BitOperatingSystem ? "amd64" : "x86",
|
|
#endif
|
|
CultureInfo.CurrentCulture.Name,
|
|
AssemblyVersionInformation.Version);
|
|
}
|
|
}
|
|
}
|