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;
56 readonly ILogger<GitHubClientFactory>
logger;
66 readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed, DateTimeOffset? Expiry)>
clientCache;
83 ILogger<GitHubClientFactory>
logger,
84 IOptions<GeneralConfiguration> generalConfigurationOptions)
88 this.logger =
logger ??
throw new ArgumentNullException(nameof(
logger));
89 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
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)...
General configuration options.
string? GitHubAccessToken
A classic GitHub personal access token to use for bypassing rate limits on requests....
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.
const uint AppTokenExpiryGraceMinutes
Minutes before tokens expire before not using them.
GitHubClientFactory(IAssemblyInformationProvider assemblyInformationProvider, IHttpMessageHandlerFactory httpMessageHandlerFactory, ILogger< GitHubClientFactory > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Initializes a new instance of the GitHubClientFactory class.
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.
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.