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. :) /// /// A connection for making HTTP requests against URI endpoints. /// 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; /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// public Connection(ProductHeaderValue productInformation) : this(productInformation, _defaultGitHubApiUrl, _anonymousCredentials) { } /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// /// /// The client to use for executing requests /// public Connection(ProductHeaderValue productInformation, IHttpClient httpClient) : this(productInformation, _defaultGitHubApiUrl, _anonymousCredentials, httpClient, new SimpleJsonSerializer()) { } /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// /// /// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise /// instance public Connection(ProductHeaderValue productInformation, Uri baseAddress) : this(productInformation, baseAddress, _anonymousCredentials) { } /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// /// Provides credentials to the client when making requests public Connection(ProductHeaderValue productInformation, ICredentialStore credentialStore) : this(productInformation, _defaultGitHubApiUrl, credentialStore) { } /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// /// /// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise /// instance /// Provides credentials to the client when making requests [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()) { } /// /// Creates a new connection instance used to make requests of the GitHub API. /// /// /// 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. /// /// /// The address to point this client to such as https://api.github.com or the URL to a GitHub Enterprise /// instance /// Provides credentials to the client when making requests /// A raw used to make requests /// Class used to serialize and deserialize JSON requests 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); } /// /// Gets the latest API Info - this will be null if no API calls have been made /// /// representing the information returned as part of an Api call 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> Get(Uri uri, IDictionary parameters, string accepts) { Ensure.ArgumentNotNull(uri, "uri"); return SendData(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, CancellationToken.None); } public Task> Get(Uri uri, IDictionary parameters, string accepts, CancellationToken cancellationToken) { Ensure.ArgumentNotNull(uri, "uri"); return SendData(uri.ApplyParameters(parameters), HttpMethod.Get, null, accepts, null, cancellationToken); } public Task> Get(Uri uri, TimeSpan timeout) { Ensure.ArgumentNotNull(uri, "uri"); return SendData(uri, HttpMethod.Get, null, null, null, timeout, CancellationToken.None); } /// /// Performs an asynchronous HTTP GET request that expects a containing HTML. /// /// URI endpoint to send request to /// Querystring parameters for the request /// representing the received HTTP response public Task> GetHtml(Uri uri, IDictionary parameters) { Ensure.ArgumentNotNull(uri, "uri"); return GetHtml(new Request { Method = HttpMethod.Get, BaseAddress = BaseAddress, Endpoint = uri.ApplyParameters(parameters) }); } public Task> Patch(Uri uri, object body) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); return SendData(uri, HttpVerb.Patch, body, null, null, CancellationToken.None); } public Task> Patch(Uri uri, object body, string accepts) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); Ensure.ArgumentNotNull(accepts, "accepts"); return SendData(uri, HttpVerb.Patch, body, accepts, null, CancellationToken.None); } /// /// Performs an asynchronous HTTP POST request. /// /// URI endpoint to send request to /// representing the received HTTP response public async Task Post(Uri uri) { Ensure.ArgumentNotNull(uri, "uri"); var response = await SendData(uri, HttpMethod.Post, null, null, null, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } public Task> Post(Uri uri) { Ensure.ArgumentNotNull(uri, "uri"); return SendData(uri, HttpMethod.Post, null, null, null, CancellationToken.None); } public Task> Post(Uri uri, object body, string accepts, string contentType) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None); } /// /// Performs an asynchronous HTTP POST request. /// Attempts to map the response body to an object of type /// /// The type to map the response to /// URI endpoint to send request to /// The object to serialize as the body of the request /// Specifies accepted response media types. /// Specifies the media type of the request body /// Two Factor Authentication Code /// representing the received HTTP response public Task> Post(Uri uri, object body, string accepts, string contentType, string twoFactorAuthenticationCode) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, twoFactorAuthenticationCode); } public Task> Post(Uri uri, object body, string accepts, string contentType, TimeSpan timeout) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); return SendData(uri, HttpMethod.Post, body, accepts, contentType, timeout, CancellationToken.None); } public Task> Post(Uri uri, object body, string accepts, string contentType, Uri baseAddress) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(body, "body"); return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, baseAddress: baseAddress); } public Task> Put(Uri uri, object body) { return SendData(uri, HttpMethod.Put, body, null, null, CancellationToken.None); } public Task> Put(Uri uri, object body, string twoFactorAuthenticationCode) { return SendData(uri, HttpMethod.Put, body, null, null, CancellationToken.None, twoFactorAuthenticationCode); } public Task> Put(Uri uri, object body, string twoFactorAuthenticationCode, string accepts) { return SendData(uri, HttpMethod.Put, body, accepts, null, CancellationToken.None, twoFactorAuthenticationCode); } Task> SendData( 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(body, accepts, contentType, cancellationToken, twoFactorAuthenticationCode, request); } Task> SendData( 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(body, accepts, contentType, cancellationToken, twoFactorAuthenticationCode, request); } Task> SendDataInternal(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(request, cancellationToken); } /// /// Performs an asynchronous HTTP PATCH request. /// /// URI endpoint to send request to /// representing the received HTTP response public async Task Patch(Uri uri) { Ensure.ArgumentNotNull(uri, "uri"); var request = new Request { Method = HttpVerb.Patch, BaseAddress = BaseAddress, Endpoint = uri }; var response = await Run(request, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Performs an asynchronous HTTP PUT request that expects an empty response. /// /// URI endpoint to send request to /// The returned public async Task Put(Uri uri) { Ensure.ArgumentNotNull(uri, "uri"); var request = new Request { Method = HttpMethod.Put, BaseAddress = BaseAddress, Endpoint = uri }; var response = await Run(request, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// /// URI endpoint to send request to /// The returned public async Task Delete(Uri uri) { Ensure.ArgumentNotNull(uri, "uri"); var request = new Request { Method = HttpMethod.Delete, BaseAddress = BaseAddress, Endpoint = uri }; var response = await Run(request, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// /// URI endpoint to send request to /// Two Factor Code /// The returned public async Task Delete(Uri uri, string twoFactorAuthenticationCode) { Ensure.ArgumentNotNull(uri, "uri"); var response = await SendData(uri, HttpMethod.Delete, null, null, null, CancellationToken.None, twoFactorAuthenticationCode).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// /// URI endpoint to send request to /// The object to serialize as the body of the request /// The returned public async Task 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(request, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Performs an asynchronous HTTP DELETE request that expects an empty response. /// /// URI endpoint to send request to /// The object to serialize as the body of the request /// Specifies accept response media type /// The returned public async Task Delete(Uri uri, object data, string accepts) { Ensure.ArgumentNotNull(uri, "uri"); Ensure.ArgumentNotNull(accepts, "accepts"); var response = await SendData(uri, HttpMethod.Delete, data, accepts, null, CancellationToken.None).ConfigureAwait(false); return response.HttpResponse.StatusCode; } /// /// Base address for the connection. /// public Uri BaseAddress { get; private set; } public string UserAgent { get; private set; } /// /// Gets the used to provide credentials for the connection. /// public ICredentialStore CredentialStore { get { return _authenticator.CredentialStore; } } /// /// Gets or sets the credentials used by the connection. /// /// /// You can use this property if you only have a single hard-coded credential. Otherwise, pass in an /// to the constructor. /// Setting this property will change the to use /// the default with just these credentials. /// 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> GetHtml(IRequest request) { request.Headers.Add("Accept", AcceptHeaders.StableVersionHtml); var response = await RunRequest(request, CancellationToken.None).ConfigureAwait(false); return new ApiResponse(response, response.Body as string); } async Task> Run(IRequest request, CancellationToken cancellationToken) { _jsonPipeline.SerializeRequest(request); var response = await RunRequest(request, cancellationToken).ConfigureAwait(false); return _jsonPipeline.DeserializeResponse(response); } // THIS IS THE METHOD THAT EVERY REQUEST MUST GO THROUGH! async Task 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> _httpExceptionMap = new Dictionary> { { 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 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); } } }