2using System.Collections.Generic;
3using System.IdentityModel.Tokens.Jwt;
5using System.Security.Cryptography;
8using System.Threading.Tasks;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
12using Microsoft.IdentityModel.Tokens;
44 readonly ILogger<GitHubClientFactory>
logger;
54 readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed)>
clientCache;
69 ILogger<GitHubClientFactory>
logger,
70 IOptions<GeneralConfiguration> generalConfigurationOptions)
73 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
74 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
76 clientCache =
new Dictionary<string, (GitHubClient, DateTimeOffset)>();
84 public async ValueTask<IGitHubClient>
CreateClient(CancellationToken cancellationToken)
91 public async ValueTask<IGitHubClient>
CreateClient(
string accessToken, CancellationToken cancellationToken)
93 accessToken ??
throw new ArgumentNullException(nameof(accessToken)),
103 =>
CreateAppClientInternal(tgsEncodedAppPrivateKey ??
throw new ArgumentNullException(nameof(tgsEncodedAppPrivateKey)));
112#pragma warning disable CA1506
114#pragma warning restore CA1506
116 GitHubClient? client;
118 DateTimeOffset? lastUsed;
122 if (String.IsNullOrWhiteSpace(accessString))
128 cacheKey = accessString;
130 cacheHit =
clientCache.TryGetValue(cacheKey, out var tuple);
132 var now = DateTimeOffset.UtcNow;
135 logger.LogTrace(
"Creating new GitHubClient...");
137 if (accessString !=
null)
141 if (repositoryIdentifier ==
null)
142 throw new InvalidOperationException(
"Cannot create app installation key without target repositoryIdentifier!");
144 logger.LogTrace(
"Performing GitHub App authentication for installation on repository {installationRepositoryId}", repositoryIdentifier);
150 Installation installation;
153 var installationTask = repositoryIdentifier.IsSlug
154 ? client.GitHubApps.GetRepositoryInstallationForCurrent(repositoryIdentifier.
Owner, repositoryIdentifier.
Name)
155 : client.GitHubApps.GetRepositoryInstallationForCurrent(repositoryIdentifier.
RepositoryId.Value);
156 installation = await installationTask;
160 logger.LogError(ex,
"Failed to perform app authentication!");
164 cancellationToken.ThrowIfCancellationRequested();
167 var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
169 client.Credentials =
new Credentials(installToken.Token);
173 logger.LogError(ex,
"Failed to perform installation authentication!");
180 client.Credentials =
new Credentials(accessString);
186 clientCache.Add(cacheKey, (Client: client, LastUsed: now));
191 logger.LogTrace(
"Cache hit for GitHubClient");
192 client = tuple.Client;
193 lastUsed = tuple.LastUsed;
194 tuple.LastUsed = now;
206 if (tuple.LastUsed <= purgeAfter)
215 "Pruned {count} expired GitHub client(s) from cache that haven't been used in {purgeAfterHours} hours.",
220 var rateLimitInfo = client.GetLastApiInfo()?.RateLimit;
221 if (rateLimitInfo !=
null)
222 if (rateLimitInfo.Remaining == 0)
224 "Requested GitHub client has no requests remaining! Limit resets at {resetTime}",
225 rateLimitInfo.Reset.ToString(
"o"));
226 else if (rateLimitInfo.Remaining < 25)
228 "Requested GitHub client has only {remainingRequests} requests remaining after the usage at {lastUse}! Limit resets at {resetTime}",
229 rateLimitInfo.Remaining,
231 rateLimitInfo.Reset.ToString(
"o"));
234 "Requested GitHub client has {remainingRequests} requests remaining after the usage at {lastUse}. Limit resets at {resetTime}",
235 rateLimitInfo.Remaining,
237 rateLimitInfo.Reset.ToString(
"o"));
250 var splits = tgsEncodedAppPrivateKey.Split(
':');
251 if (splits.Length != 2)
253 logger.LogError(
"Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length);
260 pemBytes = Convert.FromBase64String(splits[1]);
264 logger.LogError(ex,
"Failed to parse supposed base64 PEM!");
268 var pem = Encoding.UTF8.GetString(pemBytes);
270 using var rsa = RSA.Create();
274 rsa.ImportFromPem(pem);
278 logger.LogWarning(ex,
"Failed to parse PEM!");
282 var signingCredentials =
new SigningCredentials(
283 new RsaSecurityKey(rsa),
284 SecurityAlgorithms.RsaSha256)
287 CryptoProviderFactory =
new CryptoProviderFactory
289 CacheSignatureProviders =
false,
292 var jwtSecurityTokenHandler =
new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation =
false };
294 var nowDateTime = DateTime.UtcNow;
298 var jwt = jwtSecurityTokenHandler.CreateToken(
new SecurityTokenDescriptor
300 Issuer = appOrClientId,
301 Expires = nowDateTime.AddMinutes(10),
302 IssuedAt = nowDateTime,
303 SigningCredentials = signingCredentials,
306 var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt);
307 client.Credentials =
new Credentials(jwtStr, AuthenticationType.Bearer);
318 return new GitHubClient(
319 new ProductHeaderValue(
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)...
General configuration options.
string? GitHubAccessToken
A classic GitHub personal access token to use for bypassing rate limits on requests....
const string DefaultCacheKey
The clientCache KeyValuePair<TKey, TValue>.Key used in place of null when accessing a configuration-b...
readonly Dictionary< string,(GitHubClient Client, DateTimeOffset LastUsed)> clientCache
Cache of created GitHubClients and last used times, keyed by access token.
GitHubClientFactory(IAssemblyInformationProvider assemblyInformationProvider, ILogger< GitHubClientFactory > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Initializes a new instance of the GitHubClientFactory class.
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 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 .
async ValueTask< IGitHubClient > CreateClient(string accessToken, CancellationToken cancellationToken)
Create a client with authentication using a personal access token.A new IGitHubClient.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the GitHubClientFactory.
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.