2using System.Collections.Generic;
 
    3using System.IdentityModel.Tokens.Jwt;
 
    6using System.Security.Cryptography;
 
    9using System.Threading.Tasks;
 
   11using Microsoft.Extensions.Logging;
 
   12using Microsoft.Extensions.Options;
 
   13using Microsoft.IdentityModel.Tokens;
 
   16using Octokit.Internal;
 
   61        readonly ILogger<GitHubClientFactory> 
logger;
 
   66        readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed, DateTimeOffset? Expiry)> 
clientCache;
 
   84            ILogger<GitHubClientFactory> 
logger)
 
   89            this.logger = 
logger ?? 
throw new ArgumentNullException(nameof(
logger));
 
   91            clientCache = 
new Dictionary<string, (GitHubClient, DateTimeOffset, DateTimeOffset?)>();
 
 
   99        public async ValueTask<IGitHubClient> 
CreateClient(CancellationToken cancellationToken)
 
  103                cancellationToken))!;
 
  106        public async ValueTask<IGitHubClient> 
CreateClient(
string accessToken, CancellationToken cancellationToken)
 
  108                accessToken ?? 
throw new ArgumentNullException(nameof(accessToken)),
 
  110                cancellationToken))!;
 
  118            => 
CreateAppClientInternal(tgsEncodedAppPrivateKey ?? 
throw new ArgumentNullException(nameof(tgsEncodedAppPrivateKey)));
 
  127#pragma warning disable CA1506  
  129#pragma warning restore CA1506 
  131            GitHubClient? client;
 
  133            DateTimeOffset? lastUsed;
 
  137                if (String.IsNullOrWhiteSpace(accessString))
 
  143                    cacheKey = accessString;
 
  145                var now = DateTimeOffset.UtcNow;
 
  146                cacheHit = 
clientCache.TryGetValue(cacheKey, out var tuple);
 
  147                var tokenValid = cacheHit && (!tuple.Expiry.HasValue || tuple.Expiry.Value <= now);
 
  152                        logger.LogDebug(
"Previously cached GitHub token has expired!");
 
  156                    logger.LogTrace(
"Creating new GitHubClient...");
 
  158                    DateTimeOffset? expiry = 
null;
 
  159                    if (accessString != 
null)
 
  163                            if (repositoryIdentifier == 
null)
 
  164                                throw new InvalidOperationException(
"Cannot create app installation key without target repositoryIdentifier!");
 
  166                            logger.LogTrace(
"Performing GitHub App authentication for installation on repository {installationRepositoryId}", repositoryIdentifier);
 
  172                            Installation installation;
 
  175                                var installationTask = repositoryIdentifier.IsSlug
 
  176                                    ? client.GitHubApps.GetRepositoryInstallationForCurrent(repositoryIdentifier.
Owner, repositoryIdentifier.
Name)
 
  177                                    : client.GitHubApps.GetRepositoryInstallationForCurrent(repositoryIdentifier.
RepositoryId.Value);
 
  178                                installation = await installationTask;
 
  182                                logger.LogError(ex, 
"Failed to perform app authentication!");
 
  186                            cancellationToken.ThrowIfCancellationRequested();
 
  189                                var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
 
  191                                client.Credentials = 
new Credentials(installToken.Token);
 
  196                                logger.LogError(ex, 
"Failed to perform installation authentication!");
 
  203                            client.Credentials = 
new Credentials(accessString);
 
  209                    clientCache.Add(cacheKey, (Client: client, LastUsed: now, Expiry: expiry));
 
  214                    logger.LogTrace(
"Cache hit for GitHubClient");
 
  215                    client = tuple.Client;
 
  216                    lastUsed = tuple.LastUsed;
 
  217                    tuple.LastUsed = now;
 
  229                    if (tuple.LastUsed <= purgeAfter || (tuple.Expiry.HasValue && tuple.Expiry.Value <= now))
 
  238                        "Pruned {count} expired GitHub client(s) from cache that haven't been used in {purgeAfterHours} hours.",
 
  243            var rateLimitInfo = client.GetLastApiInfo()?.RateLimit;
 
  244            if (rateLimitInfo != 
null)
 
  245                if (rateLimitInfo.Remaining == 0)
 
  247                        "Requested GitHub client has no requests remaining! Limit resets at {resetTime}",
 
  248                        rateLimitInfo.Reset.ToString(
"o"));
 
  249                else if (rateLimitInfo.Remaining < 25) 
 
  251                        "Requested GitHub client has only {remainingRequests} requests remaining after the usage at {lastUse}! Limit resets at {resetTime}",
 
  252                        rateLimitInfo.Remaining,
 
  254                        rateLimitInfo.Reset.ToString(
"o"));
 
  257                        "Requested GitHub client has {remainingRequests} requests remaining after the usage at {lastUse}. Limit resets at {resetTime}",
 
  258                        rateLimitInfo.Remaining,
 
  260                        rateLimitInfo.Reset.ToString(
"o"));
 
 
  273            var splits = tgsEncodedAppPrivateKey.Split(
':');
 
  274            if (splits.Length != 2)
 
  276                logger.LogError(
"Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length);
 
  283                pemBytes = Convert.FromBase64String(splits[1]);
 
  287                logger.LogError(ex, 
"Failed to parse supposed base64 PEM!");
 
  291            var pem = Encoding.UTF8.GetString(pemBytes);
 
  293            using var rsa = RSA.Create();
 
  297                rsa.ImportFromPem(pem);
 
  301                logger.LogWarning(ex, 
"Failed to parse PEM!");
 
  305            var signingCredentials = 
new SigningCredentials(
 
  306                 new RsaSecurityKey(rsa),
 
  307                 SecurityAlgorithms.RsaSha256)
 
  310                CryptoProviderFactory = 
new CryptoProviderFactory
 
  312                    CacheSignatureProviders = 
false,
 
  315            var jwtSecurityTokenHandler = 
new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = 
false };
 
  317            var nowDateTime = DateTime.UtcNow;
 
  321            var jwt = jwtSecurityTokenHandler.CreateToken(
new SecurityTokenDescriptor
 
  323                Issuer = appOrClientId,
 
  324                Expires = nowDateTime.AddMinutes(10),
 
  325                IssuedAt = nowDateTime,
 
  326                SigningCredentials = signingCredentials,
 
  329            var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt);
 
  330            client.Credentials = 
new Credentials(jwtStr, AuthenticationType.Bearer);
 
 
  341#pragma warning disable CA2000  
  345                var clientAdapter = 
new HttpClientAdapter(() => handler);
 
  346#pragma warning restore CA2000  
  350                    return new GitHubClient(
 
  352                            new ProductHeaderValue(
 
  359                    clientAdapter.Dispose();
 
 
 
 
Represents configurable settings for a git repository.
 
const string TgsAppPrivateKeyPrefix
Prefix for TGS encoded app private keys. This is encoded in the format PREFIX + (APP_ID OR CLIENT_ID)...
 
readonly IHttpMessageHandlerFactory httpMessageHandlerFactory
The IHttpMessageHandlerFactory for the GitHubClientFactory.
 
const string DefaultCacheKey
The clientCache KeyValuePair<TKey, TValue>.Key used in place of null when accessing a configuration-b...
 
GitHubClient? CreateAppClientInternal(string tgsEncodedAppPrivateKey)
Create an App (not installation) authenticated GitHubClient.
 
async ValueTask< IGitHubClient > CreateClient(CancellationToken cancellationToken)
Create a IGitHubClient client. Low rate limit unless the server's GitHubAccessToken is set to bypass ...
 
IGitHubClient? CreateAppClient(string tgsEncodedAppPrivateKey)
Create an App (not installation) authenticated IGitHubClient.A new app auth IGitHubClient for the giv...
 
readonly SemaphoreSlim clientCacheSemaphore
The SemaphoreSlim used to guard access to clientCache.
 
GitHubClient CreateUnauthenticatedClient()
Creates an unauthenticated GitHubClient.
 
readonly IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions
The IOptionsMonitor<TOptions> of GeneralConfiguration for the GitHubClientFactory.
 
const uint AppTokenExpiryGraceMinutes
Minutes before tokens expire before not using them.
 
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the GitHubClientFactory.
 
readonly ILogger< GitHubClientFactory > logger
The ILogger for the GitHubClientFactory.
 
async ValueTask< IGitHubClient?> GetOrCreateClient(string? accessString, RepositoryIdentifier? repositoryIdentifier, CancellationToken cancellationToken)
Retrieve a GitHubClient from the clientCache or add a new one based on a given accessString .
 
GitHubClientFactory(IAssemblyInformationProvider assemblyInformationProvider, IHttpMessageHandlerFactory httpMessageHandlerFactory, IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions, ILogger< GitHubClientFactory > logger)
Initializes a new instance of the GitHubClientFactory class.
 
async ValueTask< IGitHubClient > CreateClient(string accessToken, CancellationToken cancellationToken)
Create a client with authentication using a personal access token.A new IGitHubClient.
 
readonly Dictionary< string,(GitHubClient Client, DateTimeOffset LastUsed, DateTimeOffset? Expiry)> clientCache
Cache of created GitHubClients and last used/expiry times, keyed by access token.
 
const uint ClientCacheHours
Limit to the amount of hours a GitHubClient can live in the clientCache.
 
ValueTask< IGitHubClient?> CreateClientForRepository(string accessString, RepositoryIdentifier repositoryIdentifier, CancellationToken cancellationToken)
Creates a GitHub client that will only be used for a given repositoryIdentifier .A ValueTask<TResult>...
 
Identifies a repository either by its RepositoryId or Owner and Name.
 
long? RepositoryId
The repository ID.
 
string? Name
The repository's name.
 
string? Owner
The repository's owning entity.
 
Async lock context helper.
 
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
 
For creating IGitHubClients.